Bonsai: motor wiki familiar

Introducción lírica


Una tarde, al poner las cosas en orden en un armario de pared, me top√© con una gran caja de cart√≥n. Sobrevivi√≥ a dos reubicaciones y no abri√≥ durante tantos a√Īos que olvid√© por completo lo que estaba almacenado en √©l. Result√≥ que hab√≠a fotos, en √°lbumes, en sobres de una tienda, y algunas eran as√≠.

Muchas fotograf√≠as fueron tomadas hace m√°s de setenta a√Īos. Uno era abuelo: en su √©poca de estudiante, todav√≠a joven y guapo, con gafas absolutamente destructivas . "Wow, mi abuelo usaba ropa hipster incluso antes de que se convirtiera en la corriente principal", pens√©, e involuntariamente sonre√≠. Lo reconoc√≠ de inmediato, pero luego tom√© fotos de personas de las que no recuerdo nada. En los rasgos faciales, puedes adivinar vagamente la relaci√≥n, y eso es todo.



Cuando ten√≠a quince a√Īos, mi abuela mostr√≥ repetidamente estas tarjetas y habl√≥ sobre los que est√°n representados en ellas. Desafortunadamente, el valor de tales historias se entiende solo cuando no hay nadie para contarlas. En ese momento, por d√©cima vez fue absolutamente desinteresado escuchar algunos cuentos cubiertos de musgo sobre los a√Īos anteriores a la guerra, los rechac√© y les hice caso. Ahora, de repente, al darme cuenta de que parte de la historia de mi familia estaba irremediablemente perdida, tuve la idea de sistematizar y preservar lo que quedaba.

La solución ideal para almacenar datos familiares me pareció un híbrido de un motor wiki y un álbum de fotos. No había soluciones adecuadas ya hechas, así que tuve que escribir la mía. Se llama Bonsai y es de código abierto bajo la licencia MIT. Luego habrá una historia sobre cómo está organizada y cómo usarla, así como la historia de su desarrollo y un poco de DRAMA .



Otra bicicleta?


Hoy en día, hay muchas herramientas que le permiten crear árboles genealógicos y catalogar información sobre parientes. Se dividen condicionalmente en dos grandes categorías: servicios en línea y aplicaciones de escritorio.

En el caso de una aplicaci√≥n de escritorio, la base de datos generalmente se almacena como un archivo en el disco. Abre la aplicaci√≥n y la repone en modo de usuario √ļnico. Si es necesario, los datos se pueden exportar para hacer una copia de seguridad o transferirlos a otro sistema (por ejemplo, en formato GEDCOM ). De los que vi, el m√°s agradable de usar parec√≠a Gramps (gratis) y el Life Tree dom√©stico (requiere una compra √ļnica).

El lado opuesto del espectro son los servicios web. Almacenan sus datos en servidores remotos y cobran una tarifa de uso periódica. Dado que este es un producto comercial con una base centralizada y una buena monetización, los servicios de este plan le brindan la oportunidad, por ejemplo, de buscar familiares perdidos mediante pruebas de ADN o registros de archivo.

Los pros y los contras de ambas opciones son bastante obvios. En el primer caso, almacena la base de datos localmente y controla completamente el acceso a ella y la creación de copias de seguridad. Si la aplicación es de código abierto, si es necesario, incluso puede agregarle funcionalidad adicional. Sin embargo, trabajar con dicha base de datos juntos o ver datos desde otro dispositivo será difícil. En el segundo, por el contrario, el acceso es desde cualquier dispositivo, pero usted cede sus datos a terceros y espera su decencia. En la historia de mi familia no hay secretos comprometedores y terribles, sin embargo, todavía considero que esta información es puramente personal y, en principio, no quiero que nadie más la almacene o analice.

Dadas las deficiencias de ambos enfoques, podemos formular una lista de requisitos para el motor "ideal":

  1. Aplicación web alojada en su propio servidor
  2. Creación de artículos sobre personas, mascotas, lugares, eventos, etc. como un wiki
  3. Descargar medios
  4. Marcas de personas en fotos y videos.
  5. Construcción automática de árboles genealógicos
  6. Calendario con todas las fechas importantes.
  7. Herramientas para coedición y relleno

Para ser justos, logré encontrar varios proyectos con una implementación autohospedada, pero estaban en un estado deplorable: la apariencia se congeló a mediados de la década de 2000, no había un conjunto completo de la funcionalidad necesaria y no quería profundizar en los scripts heredados en PHP. Además, el proyecto de mascota anterior había terminado y había un deseo de asumir algo nuevo.

La regla de oro dice: si quieres hacerlo bien, ¬°hazlo t√ļ mismo!

Las tecnologías utilizadas se seleccionaron de acuerdo con tres criterios: mi experiencia con ellas, popularidad y sin apertura. Aquí está el resultado:

  • Rantime : .NET Core 2.1
  • Back - end : ASP.NET Core MVC
  • Base de datos : PostgreSQL
  • L√≥gica frontend : parcialmente Vue, parcialmente jQuery.
  • Estilos frontend : Bootstrap + Sass

Los roles de apoyo incluyen Elasticsearch para b√ļsqueda de texto completo y ffmpeg para tomar capturas de pantalla del video.

Esquema de datos


Los objetos principales en la base de datos Bonsai son una página y un archivo multimedia . Están conectados por una relación de muchos a muchos a través de las marcas . Una etiqueta puede tener un título sin un enlace, por ejemplo, si necesita etiquetar a alguien en una foto, pero no hay información en una página completa al respecto.



Además del texto libre, la página puede contener datos que se ingresan en campos especiales en el panel de administración. Los hechos adicionales se calculan sobre los hechos: por ejemplo, si indica la fecha de nacimiento de la persona, se marcará en el calendario y su página mostrará la edad actual (o la esperanza de vida, si también se indica la fecha de fallecimiento), el sexo puede usarse para determinar el nombre correcto de la relación ("padre "O" madre "en lugar de los" padres "comunes), y así sucesivamente. Los hechos se almacenan en la base de datos como un documento JSON.

Hay cinco tipos de páginas para elegir: persona, mascota, evento, lugar, etc. La lista de datos disponibles depende del tipo de página: por ejemplo, "educación" es relevante solo para una persona, "fecha de nacimiento" - para una persona y un animal, y "dirección" - solo para un lugar.

Las páginas están interconectadas por relaciones : "padre", "cónyuge", "amigo", "propietario", "residente" y muchos otros. Algunas relaciones pueden ser limitadas en el tiempo (cónyuge, propietario, residente), otras se consideran permanentes.

Cuando guarda cualquier p√°gina o relaci√≥n, se verifica la coherencia del modelo resultante. Por ejemplo, los a√Īos de vida de los c√≥nyuges deben superponerse , una persona no puede tener m√°s de un padre biol√≥gico de cada sexo y tampoco puede convertirse en su propio padre . Los matrimonios del mismo sexo, sin embargo, son permisibles.

La edición de una página, un archivo multimedia o una relación guarda el cambio en la base de datos. Esto le permite guardar el historial de ediciones y revertirlas si es necesario.

Relación


El parentesco es uno de los conceptos más antiguos de la sociedad. Ya en el idioma pre-indoeuropeo, había muchos nombres para ellos, que, en una forma ligeramente modificada, migraron a los idiomas modernos de varios grupos: la palabra "madre" será entendida por el ruso, el inglés y el chino.

Hay muchas opciones para el parentesco, pero las básicas son tres: padre , hijo y cónyuge . Le permiten construir un gráfico dirigido de la familia donde estas relaciones son aristas y las personas son nodos. En esta columna, puede expresar cualquier otra relación, conociendo el camino entre los participantes y su género: por ejemplo, para identificar al abuelo de alguien, primero debe encontrar a su padre (cualquier género), y luego el padre de este padre (hombre), y así sucesivamente.

En el panel de administraci√≥n de Bonsai, puede ingresar las relaciones de estos tres tipos b√°sicos. Lo opuesto se crear√° autom√°ticamente para cada relaci√≥n: padre por hijo, c√≥nyuge por c√≥nyuge, due√Īo por mascota. El motor calcula todas las relaciones adicionales y se muestran en la barra lateral de la p√°gina:



Para calcular la relación, se utiliza un recorrido de gráfico elemental y los nombres de relación se establecen en forma de un DSL especial:

public static RelationDefinition[] ParentRelations = { new RelationDefinition("Parent:m", ""), new RelationDefinition("Parent:f", ""), new RelationDefinition("Parent Child:m", "", ""), new RelationDefinition("Parent Child:f", "", ""), new RelationDefinition("Parent Parent:m", "", ""), new RelationDefinition("Parent Parent:f", "", "") }; 

Incluso una persona puede tener muchos parientes directos. Bonsai divide los enlaces en los siguientes grupos:

  1. La relación de sangre más cercana es la familia en la que la persona creció: madre y padre, abuelos, hermanos y hermanas. Si observa el gráfico, esta es la ruta 1-2 pasos hacia arriba y 1 hacia los lados.
  2. Familia propia : un grupo por cada c√≥nyuge e hijos de √©l. Esto tambi√©n incluye a los familiares del c√≥nyuge: suegra, cu√Īado y similares.
  3. Otros : parientes más distantes (nietos, tíos, tías) y lazos no familiares (amigos, colegas).

A veces, una forma de determinar la pertenencia a un grupo no es suficiente. Los datos pueden estar incompletos, pero a√ļn deben mostrarse de la manera m√°s adecuada posible. Considere el siguiente gr√°fico de hermanos:



Como podemos ver, dos esposas (Vera y Galina) y un hijo (Boris) est√°n indicados para Alexander, pero no sabemos cu√°l de las esposas es la madre del ni√Īo; tal vez esta sea una especie de tercera mujer, pero a√ļn no ha sido agregada. Para tales casos, se pueden indicar varias rutas que deber√≠an existir o no, y est√°n marcadas con signos + y - respectivamente:

 new RelationDefinition("Spouse Child+Child", "||", "") new RelationDefinition("Spouse Child-Child:m", "") new RelationDefinition("Spouse Child-Child:f", "") 

√Ārbol geneal√≥gico


Cualquier motor de genealogía decente debería ser capaz de construir un árbol genealógico. Esta es la forma más visual de mostrar información general sobre las personas y sus relaciones familiares. Los datos se almacenan en la base de datos en forma de un gráfico dirigido y, en teoría, debería ser fácil de visualizar. En la práctica, fue con la exhibición del árbol que surgieron las mayores dificultades.

Aquí hay algunos ejemplos de cómo se verían los árboles genealógicos:



√Ārbol geneal√≥gico de Targaryenov. Muy compacto, porque est√° hecho a mano. Generar tal √°rbol a partir de datos arbitrarios ser√° extremadamente dif√≠cil autom√°ticamente.



Dioses griegos La representaci√≥n gr√°fica se genera a partir de una sintaxis de reducci√≥n especial , en la que a√ļn debe organizar manualmente todos los bloques y dibujar los enlaces entre ellos. Un poco como el arte ASCII.



Presentaci√≥n de un √°rbol en forma de diagrama semicircular. Se genera f√°cilmente de forma autom√°tica, pero solo tiene en cuenta antepasados ‚Äč‚Äčdirectos.

Miré a través de muchas opciones. Lo más estéticamente agradable fue en el sitio web MyHeritage:



La representación de dicho árbol se puede dividir en tres pasos condicionales: obtener datos de la base de datos, organizar bloques / líneas de conexión y mostrarlos directamente en la página. Si todo fue trivial con el primer y tercer paso, entonces en el segundo tropecé.

Los intentos de lanzar una soluci√≥n hecha a s√≠ misma a toda prisa terminaron en un fiasco completo. Una disposici√≥n competente de elementos gr√°ficos es un √°rea tan compleja que las disertaciones est√°n escritas en ella, y los componentes terminados son como un apartamento en Mosc√ļ. De acuerdo, no podr√°s escribir t√ļ mismo, pero ¬Ņseguramente hay soluciones gratuitas decentes?

La mayoría de mis esperanzas estaban en la biblioteca D3.js. Quizás esto es lo primero que viene a la mente si necesita dibujar un gráfico o cuadro en una página web. Por desgracia, entre más de trescientos (!) Ejemplos en la wiki, no había uno más o menos similar a un árbol con MyHeritage.

El siguiente paso fue sumergirse en bibliotecas que no estaban involucradas en el renderizado, sino en el c√°lculo de la disposici√≥n √≥ptima de los elementos en el gr√°fico. La mayor√≠a de ellos ofrecen el llamado dise√Īo de la Fuerza . Este es un enfoque muy simple, que se basa en f√≥rmulas f√≠sicas: los nodos de la gr√°fica est√°n representados por cuerpos el√°sticos, y las l√≠neas de conexi√≥n est√°n representadas por resortes. Se puede reconocer f√°cilmente por su animaci√≥n caracter√≠stica: el gr√°fico parece "enderezarse" sobre la marcha, y esto no es una caracter√≠stica adicional, sino una consecuencia inevitable de la naturaleza de simulaci√≥n del algoritmo. El enfoque de dise√Īo forzado es bueno para visualizar datos sin una jerarqu√≠a clara (por ejemplo, conexiones en redes sociales), pero el √°rbol geneal√≥gico de esta forma parece defectuoso.

Otra opci√≥n considerada es la biblioteca Graphviz . El resultado de su trabajo puede reconocerse f√°cilmente por las flechas caracter√≠sticas. Se utiliza un lenguaje especial DOT para describir el gr√°fico. Los casos de prueba se ven a√ļn m√°s o menos, pero hay problemas con los datos reales: las flechas "se rompen" y se conectan en √°ngulos extra√Īos, el gr√°fico se desliza hacia arriba y no puede ajustarlo y no puede evitarlo.

Al no haber encontrado una solución adecuada por mi cuenta, decidí pedirla por cuenta propia, y luego comenzó el mismo DRAMA .

El pedido se realiz√≥ en la ma√Īana del 22 de octubre, y en una hora recibi√≥ varias respuestas. Uno de los encuestados se llamaba Vladislav; envi√≥ un ejemplo de una soluci√≥n similar y prometi√≥ completar la tarea en un d√≠a . Esta velocidad me pareci√≥ sospechosa, pero esperaba su experiencia y para m√≠ mismo le di al chico un error de una semana. Los primeros d√≠as, Vladislav hizo preguntas adicionales, sin dejar de sorprenderme con una profunda inmersi√≥n en el proyecto y una actitud atenta a los detalles, y luego desapareci√≥. Se despert√≥ el 1 de noviembre, se disculp√≥ por la desaparici√≥n forzada por razones familiares y envi√≥ un enlace con una versi√≥n beta que se parec√≠a bastante a lo que quer√≠a si no fuera por el nodo en las l√≠neas de conexi√≥n en el centro:



La desaparici√≥n del int√©rprete siempre es una llamada de atenci√≥n, pero nunca se sabe, sucede algo, porque √©l hizo algo. ¬°Que contin√ļe! Envi√© un prepago y comenc√© a esperar mejoras. Despu√©s de un par de d√≠as, Vladislav escribi√≥ que no pod√≠a solucionar el problema y luego desapareci√≥ nuevamente, esta vez durante tres semanas. Durante este tiempo, no hizo nada y se neg√≥ a devolver el anticipo, porque "la tarea fue realizada por un est√ļpido ex amigo que lo decepcion√≥ y no le devolvi√≥ el dinero". Despu√©s de un par de preguntas aclaratorias, el desafortunado delegado dej√≥ de intentar disculparse y se call√≥. As√≠ que ahora vivimos, de vez en cuando le recuerdo la deuda, y en respuesta env√≠a una captura de pantalla de la aplicaci√≥n bancaria, dicen, "no hay dinero, pero tan pronto como sea, de inmediato". ¬°Deseo que Vladislav tenga √©xito en los negocios y enriquecerse m√°s r√°pido!

Lanz√≥ al ni√Īo - menos en karma!

Perder dinero no fue tan molesto, pero pas√≥ un mes, y la tarea no se movi√≥, y ahora no hab√≠a ning√ļn lugar para esperar ayuda. En primer lugar, estaba enojado conmigo mismo: tom√© el camino de menor resistencia, viol√© la regla de oro , y aqu√≠ est√° el resultado. Lleno de ira justa, nuevamente me sent√© a estudiar bibliotecas para dibujar gr√°ficos y ¬°he aqu√≠! - De repente encontr√≥ exactamente lo que necesita.

La biblioteca se llamaba Eclipse Layout Kernel , abreviado ELK. Como puede suponer, se usa para mostrar diagramas en el Eclipse IDE, pero también se puede usar de forma autónoma. En general, está escrito en Java, pero hay una versión transmitida en JS. Sí, su código es una pesadilla y pesa un megabyte y medio, pero estas deficiencias se pueden perdonar por el hecho de que simplemente funciona y hace exactamente lo correcto. La interfaz es elemental: los nodos, bordes y configuraciones se transmiten a la entrada, y en la salida obtenemos las coordenadas. Puede dibujar un árbol con ellos de cualquier manera conveniente: elegí SVG para conectar líneas y divs con posicionamiento absoluto para bloques.

La integración de la biblioteca y la selección de configuraciones óptimas tomó dos noches en la fuerza. Esto, por supuesto, no es "un día", como prometió mi desafortunado y arrogante profesional independiente, sino bastante cerca. Como resultado, Bonsai pudo mostrar el árbol aproximadamente de esta forma:



Ahora el √ļnico problema que queda es el tiempo de procesamiento. ELK utiliza un algoritmo iterativo: puede acercarse a la ubicaci√≥n √≥ptima al dedicar m√°s tiempo. En un √°rbol de 20-30 elementos, un buen resultado requiere aproximadamente 5 segundos. Debido a esto, una p√°gina con un √°rbol se abre cada vez durante mucho tiempo, y r√°pidamente comienza a molestar. Para la pr√≥xima versi√≥n, el c√°lculo se transferir√° al backend para que se pueda hacer una vez al cambiar la p√°gina y el almacenamiento en cach√©.

B√ļsqueda de texto completo


Un sistema para almacenar informaci√≥n textual ser√≠a in√ļtil sin una conveniente b√ļsqueda de texto completo. Bonsai utiliza la base de datos PostgreSQL, por lo que lo primero que decid√≠ fue comprobar qu√© pod√≠a ofrecer de inmediato. Otra decepci√≥n: tsvector hace frente a palabras comunes, pero se niega a buscar lo m√°s importante: nombres y apellidos:

 SELECT to_tsvector('') @@ to_tsquery(''), -- true to_tsvector('') @@ to_tsquery(''), -- false to_tsvector('') @@ to_tsquery(''), -- false to_tsvector('') @@ to_tsquery(''), -- false to_tsvector('  ') @@ to_tsquery('') -- false 

Los trigramas tampoco dieron nada bueno. Al final, me decid√≠ por una opci√≥n bastante esperada: ElasticSearch + Russian Morfology . Result√≥ ser muy inconveniente trabajar con √©l desde .NET, pero se las arregla con un s√≥lido cinco con una b√ļsqueda por nombre.

Imperfección consciente


Al trabajar en un proyecto, las situaciones ocurrían regularmente cuando un perfeccionista interno estaba furioso por la solución elegida. El área temática es bastante no estándar y las "buenas maneras" generalmente aceptadas no siempre funcionan.

Por ejemplo, ¬Ņqu√© sucede cuando abrimos cualquier p√°gina?

  1. El texto de la página se compila de Markdown a HTML. Si el texto contiene enlaces a otras páginas y archivos multimedia, tendrá que ir a la base de datos para obtener más información.
  2. Los hechos se deserializan del JSON en el que se almacenan en la base de datos, en el modelo de vista.
  3. Las relaciones están determinadas. Para hacer esto, desde la larga base de datos, es necesario obtener el gráfico de conexión completo y encontrar nodos en él de acuerdo con una lista de rutas previamente conocida.

A primera vista, parece una operaci√≥n terriblemente dif√≠cil, pero en realidad no se debe a la cantidad relativamente peque√Īa de datos. ¬ŅCu√°ntos parientes puedes recordar y quieres escribir? Intente contarlos por inter√©s y descubra que ser√° muy dif√≠cil marcar al menos un centenar. ¬ŅY cu√°ntas personas quieren dar acceso? ¬°Incluso un n√ļmero astron√≥micamente grande para una familia es mil personas! - Seg√ļn los est√°ndares de las bases de datos modernas, sigue siendo rid√≠culo.

Por supuesto, el modelo de vista de página compilada todavía se almacena en caché la primera vez que se abre y se reutiliza en los posteriores, principalmente porque era muy fácil de implementar. La regla de invalidación de caché para los cambios en el panel de administración también se toma de la manera más simple posible: si cambiamos solo el texto y algunos datos locales (lista de idiomas, tipo de sangre, color de cabello, etc.), simplemente restablezca esta página específica. Con cualquier otro cambio (nombre de la página, fecha de nacimiento o sexo, agregando o cambiando cualquier conexión), el caché se restablece por completo . Sí, esta no es la forma más inteligente de limpiar. Sí, seguro, podría escribir un algoritmo complejo que restablecería solo lo que necesita, pero para este proyecto no justificaría los costos.

El proyecto no admite la localizaci√≥n y el cambio de apariencia, la autorizaci√≥n funciona en OAuth en Facebook \ Google, y el panel de administraci√≥n se realiza en los formularios habituales, y no en un marco de SPA basado en la √ļltima moda. Todo esto podr√≠a realizarse o mejorarse, pero no habr√≠a resuelto ning√ļn problema y, por lo tanto, se habr√≠a perdido el tiempo.

Mirando hacia el futuro


Otra raz√≥n por la que no tiene sentido invertir en la complejidad del dispositivo del motor es la naturaleza ef√≠mera de la implementaci√≥n en comparaci√≥n con los datos que almacena. Solo piense por un momento: la web en su forma actual ha existido durante casi veinte a√Īos, y la historia familiar ha existido durante siglos . Nadie ha resuelto este problema simplemente porque la industria de la tecnolog√≠a de la informaci√≥n en s√≠ misma existe mucho menos. Que se puede hacer

El motor tendr√° que ser reescrito regularmente desde cero, al igual que durante miles de a√Īos, los monjes han sido dif√≠ciles de copiar textos de libros en ruinas a otros nuevos. La √ļnica diferencia es que el libro puede durar cien a√Īos con un manejo adecuado y la aplicaci√≥n, con una solidez de 15-20 a√Īos. Espero que en veinte a√Īos todav√≠a pueda hacerlo yo mismo, pero en otros veinte a√Īos mis hijos o nietos tendr√°n que hacerlo. Me gustar√≠a dejarles una fuente simple, comprensible y documentada.

En las primeras etapas del dise√Īo, quer√≠a incrustar un cierto lenguaje similar al SQL en el motor, con la ayuda de la cual pude obtener respuestas a preguntas espec√≠ficas: "cu√°l es el porcentaje de mis antepasados ‚Äč‚Äčcon ojos azules", "cuando Ivan compr√≥ el primer auto", etc. Esta idea tuvo que ser abandonada porque requerir√≠a, en lugar del texto sin formato, ingresar toda la informaci√≥n en una determinada forma formal, y solo una descripci√≥n de este tipo llevar√≠a a√Īos. Por otro lado, la comprensi√≥n del lenguaje natural est√° ganando impulso. No me sorprender√° si dentro de diez o dos a√Īos ser√° posible pedirle a Siri que lea el texto por usted, siga los enlaces y, como resultado, presente un extracto de los hechos. Chicos, empujen!

¬ŅC√≥mo intentarlo?


Lamentablemente, no puedo proporcionar un enlace a la demostraci√≥n finalizada: no hay ning√ļn servidor que pueda soportar el efecto habra. Pero hay algunas capturas de pantalla visuales (se puede hacer clic en las im√°genes).



Si Bonsai te pareci√≥ √ļtil y quieres ejecutarlo t√ļ mismo, el c√≥digo fuente se puede descargar desde Github:

https://github.com/impworks/bonsai

Las instrucciones detalladas de instalación se proporcionan en el archivo Léame. Necesitarás esto:

  1. .NET Core 2.1+
  2. PostgreSQL 10+
  3. ElasticSearch 5.xy el complemento de morfología rusa
  4. Aplicación de Facebook o Google para la autorización de oAuth

Después del primer lanzamiento, se crean varias páginas de prueba y fotos en la base de datos. Para la producción, este comportamiento no es necesario y el indicador lo deshabilita en la configuración.

Hace apenas un mes, lancé mi propia instancia y comencé a ejecutarla, obteniendo datos reales. Se encuentra cierta aspereza, pero de lo contrario estoy completamente satisfecho con el resultado. Ahora el proyecto se desarrollará y finalizará gradualmente. Las tareas principales son acelerar la visualización del árbol, permitir la descarga de documentos en forma de PDF y agregar ajustes de los derechos de acceso. Sería bueno mejorar la usabilidad del panel de administración en algunos lugares o reconocer automáticamente las caras en la foto, pero esto no es exacto .

Source: https://habr.com/ru/post/438622/


All Articles