Gramplets.es - dsblank/gramps-connect GitHub Wiki

🌐 English · Deutsch · Français · Nederlands · 简体中文

Gramplets y la tienda de complementos

Los Gramplets son el sistema de complementos de Gramps Connect: pequeños programas de Python que consultan el árbol y muestran un resultado: una tabla, un diagrama, texto simple o HTML. Los hay de dos clases: los Gramplets, que se ejecutan desde el menú Gramplets en una ventana propia, y los Gramplets de vista, que se sitúan en un panel bajo una lista (Persona, Familia o cualquier otro tipo de objeto) y siguen lo que usted selecciona allí. Lo que los hace poco habituales es dónde se ejecutan: cada uno es Python real, que se ejecuta directamente en la pestaña del navegador con Pyodide (CPython compilado a WebAssembly), sobre wheels compiladas localmente del propio modelo de datos gramps.gen.lib de Gramps. No se instala nada en su equipo y nada se ejecuta en un servidor; véase Arquitectura.

Eso también significa que todos los Gramplets son editables. Abra uno desde el editor de Gramplets de la aplicación y estará viendo (y podrá cambiar) su código fuente real, no una caja negra detrás de un panel de configuración. Cambie el aspecto de un diagrama, ajuste lo que busca una consulta o escriba uno nuevo desde cero, todo sin salir de la aplicación.

Quién puede usar los Gramplets

Cualquiera que haya iniciado sesión en el árbol puede ejecutar sus Gramplets, desde el menú Gramplets o en el panel «Gramplets de vista» de una lista. Solo el propietario del árbol o un administrador del sitio puede escribirlos: crear uno, editar su código o instalar, actualizar o quitar uno a través de la tienda de Gramplets. Para todos los demás, la tienda, Nuevo Gramplet… y los lápices de edición no aparecen en absoluto.

Rol Ejecutar Gramplets Escribir / instalar Gramplets
Invitado ✓ (salvo los privados) —
Miembro ✓ —
Colaborador ✓ —
Editor ✓ —
Propietario ✓ ✓
Administrador ✓ ✓

Escribir un Gramplet exige más que los derechos habituales de un editor porque su código se ejecuta en el navegador de todos los que lo usan, así que cambiarlo afecta al árbol para todos, no solo a sus propias ediciones. Un Gramplet se guarda como un objeto multimedia normal con la etiqueta Gramplet, así que se aplican las reglas de privacidad habituales: si está marcado como privado, los invitados no lo verán.

Lo mismo ocurre con Permitir cambios en los datos (véase Cambiar datos): solo un propietario o administrador puede activarlo para un Gramplet. Cualquiera puede hacer una prueba de un Gramplet así, que no guarda nada; guardar sus cambios exige derechos de editor o superiores (los propios permisos del servidor para añadir, editar y eliminar), así que para invitados, miembros y colaboradores el botón Ejecutar de la ventana está desactivado.

Explorar e instalar

La tienda de Gramplets es un catálogo navegable dentro de la aplicación; se abre desde Gramplets → Tienda de Gramplets…. Tiene dos pestañas, Gramplets y Gramplets de vista; instale, actualice o quite cualquier entrada con un clic. Un Gramplet instalado aparece de inmediato en el menú Gramplets (agrupado por su categoría); un Gramplet de vista instalado se añade a una lista con el + del panel «Gramplets de vista» de esa lista. Se publica de forma independiente de las versiones de Gramps Connect: el catálogo es contenido estático (gramplet-store/ en el repositorio) que la aplicación obtiene en tiempo de ejecución, no algo incluido en su compilación, así que pueden aparecer Gramplets nuevos en la tienda sin que nadie tenga que actualizar el propio Gramps Connect.

Entre los ejemplos que ya hay en el catálogo están estos Gramplets, que se ejecutan desde el menú Gramplets:

Gramplet Qué hace
Age at Death by Decade Diagrama de la mediana de la edad al morir para cada década de nacimiento o defunción
Mark Unsourced People Private Marca como privadas a todas las personas sin ninguna fuente; cambia datos, así que abrirlo es una prueba que enumera quién cambiaría, y Ejecutar lo guarda (véase Cambiar datos)
People Without Sources Todas las personas sin ninguna cita propia ni en ninguno de sus eventos (un recorrido de todo el árbol con barra de progreso)
Possible Duplicate People Personas con el mismo nombre nacidas con pocos años de diferencia, para revisarlas y fusionarlas
Tag People Añade una etiqueta que usted elige de una lista a todas las personas del filtro de Personas actual; cambia datos, así que es una prueba hasta que pulse Ejecutar

y estos Gramplets de vista:

Gramplet de vista Qué hace
Age-at-Death Histogram Histograma interactivo de plotly de la edad al fallecer en todo el árbol (o en la lista filtrada actual)
All Relationships Cálculos de relaciones en todo el árbol
Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes Paneles de detalles al estilo de los Gramplets de escritorio para el registro seleccionado
Born In Búsqueda por lugar de nacimiento
Crossing Relationships Encuentra relaciones que cruzan generaciones de formas poco habituales
GOQL Query Escriba una cláusula where de GOQL en bruto y elija expresiones de campo como columnas
Hello Table El ejemplo inicial mínimo
People Search Una búsqueda en vivo de Personas mientras escribe, por nombre, sexo y fecha de defunción
Phonebook Surname Sort Ordenación de apellidos que ignora los acentos (p. ej. «Müller» se ordena junto a «Muller»)
Relationship / Relationships Cómo están emparentadas dos personas seleccionadas
Selected Record Reacciona al registro que esté abierto en ese momento
To-Do / Todo Tracker Lleva el control de las tareas de investigación pendientes en todo el árbol
Tree Statistics Un tablero con recuentos de registros y porcentajes derivados

En cada vista de lista (Personas, Familias, Eventos, …), los Gramplets de vista que ha añadido a esa vista aparecen como pestañas en un panel plegable Gramplets de vista bajo la tabla. El panel empieza plegado, y un Gramplet de vista solo se ejecuta mientras el panel está abierto: al expandirlo se ejecuta la pestaña seleccionada. La primera ejecución de una sesión también carga el entorno de Python del navegador, así que puede tardar unos segundos; las siguientes son rápidas. Mantener el panel plegado no cuesta nada, ya que no se carga Python hasta que lo abre.

Gramplets en su propia ventana

Un Gramplet (a diferencia de un Gramplet de vista) no está ligado a ninguna lista. Elija uno en el menú Gramplets (agrupados en submenús por categoría cuando hay más de una categoría) y se abre en una ventana flotante:

  • Se ejecuta al abrirse. Un Gramplet que necesita datos primero (un apellido, un número de generaciones) dibuja sus propios campos y un botón Ejecutar, con los mismos widgets que usan los Gramplets de vista.
  • Los Gramplets que cambian datos (marcados con ✎ en el menú) hacen una prueba al abrirse y tienen botones Prueba / Ejecutar y Deshacer; véase Cambiar datos.
  • Ve la lista que hay detrás. Si hay una lista abierta en la vista principal, el Gramplet ve su registro seleccionado y su filtro actual, tal como estaban al empezar la ejecución; navegar a otro sitio después no cambia una ejecución en curso.
  • Progreso y Cancelar. Un Gramplet más largo muestra una barra de progreso mientras trabaja; Cancelar lo detiene de inmediato y conserva lo que hubiera producido hasta entonces.
  • Siga trabajando mientras tanto. Minimice una ventana (▾) y se convierte en una pequeña ficha en la parte inferior de la pantalla que muestra cuánto lleva; la ejecución continúa. Cuando un Gramplet minimizado termina, recibe una notificación con un botón Mostrar. Pueden ejecutarse varios Gramplets a la vez (hasta cinco ventanas), cada uno de forma independiente, y siguen abiertos mientras pasa de una vista a otra.
  • Mover, redimensionar, maximizar. Arrastre una ventana por su barra de título, cambie su tamaño desde la esquina o maximícela (⤢, o doble clic en el título). Cada Gramplet recuerda el tamaño y la posición de su ventana en su navegador.
  • Edición. Para el propietario del árbol o un administrador del sitio (véase Quién puede usar los Gramplets), la barra de título de la ventana tiene un lápiz (✎) que abre el editor de Gramplets; al guardar, la ventana se reinicia con sus cambios.
  • Cerrar. Para volver a ejecutar un Gramplet desde el principio, cierre su ventana y elíjalo de nuevo en el menú. Cerrar una ventana que todavía se está ejecutando pide confirmación antes, y lo mismo ocurre al salir de la página o recargarla; cerrar la pestaña del navegador detiene todas las ejecuciones.

Los resultados solo se muestran en la ventana; todavía no se guardan en ningún sitio.

Escribir un Gramplet

El código de un Gramplet se ejecuta en un entorno aislado con un conjunto de funciones y objetos ya disponibles: no hace falta importar nada para llegar al árbol. El directorio gramplet_examples/ del repositorio es un recorrido numerado por la API, del más sencillo al más avanzado; la misma referencia está disponible dentro de la propia aplicación mediante el botón de ayuda (i) «Writing a Gramplet» del editor de Gramplets.

El Gramplet útil más pequeño

matches = people(
    "gender == Person.MALE and birth.date.sortval >= Date('Jan 1, 1900')",
    limit=25,
)

columns("Person")
for person in matches:
    row(person)
  • people(where=None, what=None, order=None, limit=50) obtiene las Personas que coinciden con una consulta, como registros completos, en una sola llamada (o, con what=["gramps_id", "primary_name"], solo esos campos); y hay un families()/events()/places()/repositories()/sources()/ citations()/media()/notes()/tags() para cada uno de los demás tipos de objeto, con la misma firma, solo que con otra tabla.
  • where= se escribe en GOQL: exactamente la misma sintaxis que el cuadro de búsqueda de cualquier vista de lista, incluida la posibilidad de recorrer relaciones (birth.date.sortval) aunque el campo por el que filtra no sea uno de los que muestra.
  • No hace falta await aunque por debajo sea una llamada de red real: se inserta automáticamente.
  • row(*values) añade una fila a la tabla. Pasar un objeto Persona/Evento/Lugar/... completo (en lugar de un campo elegido a mano) lo muestra como un enlace en el que se puede hacer clic, que ya muestra su nombre/título completo y su ID de Gramps; al hacer clic se abre una ventana emergente para ver ese registro en Lista, Mapa, Gráfico o Cronograma.
  • name_of(person) es el nombre de una persona como texto: nombre de pila y luego apellidos («Root Ancestor»), tal como los etiqueta row(), para un título o una frase. También acepta uno de sus nombres, p. ej. name_of(person.alternate_names[0]).

Cada registro que devuelven people()/families()/etc. es un «DataDict» de Gramps: un dict normal con los campos del objeto, pero con acceso por punto encima: person.primary_name.first_name es el mismo valor que person["primary_name"]["first_name"]. Recurra a esto siempre que quiera calcular algo a partir de un campo o mostrar uno que la propia representación de row() no muestra. row(), html() y print() se pueden llamar tantas veces como quiera, en cualquier orden: todo aparece en el orden en que lo añadió.

Imprimir un registro nunca falla: print(person) o f"{person}" muestra un registro completo tal como lo etiqueta row() («Root Ancestor [I0001]»), una fecha como fecha, y cualquier otra cosa (un resultado de filter(), person.primary_name) como sus campos simples.

Contar sin coste y sin descargar el árbol

total_people = db.get_number_of_people()
women = db.get_number_of("person", where="gender == Person.FEMALE")
no_birth_date = db.get_number_of("person", where="birth.date.sortval is None")

db.get_number_of_<type>() es un recuento barato de todo el árbol: el total llega en una cabecera de la respuesta y no se descarga realmente ningún registro. db.get_number_of(object_type, where=...) es el equivalente condicional, para cuando el propio recuento necesita un filtro.

Recorrer relaciones en una consulta, y volver para mostrarlas

Una condición where= puede recorrer una relación para filtrar por ella (el evento de nacimiento de una persona, el padre de una familia, el lugar de un evento), aunque people()/families()/etc. siempre devuelven un objeto completo y aislado, sin forma de pedir directamente un campo relacionado:

born_and_died_same_place = people(
    "birth.place.title == death.place.title and birth.place.title is not None",
    order=[{"column": "surname", "direction": "asc"}],
    limit=50,
)

Obtener un campo relacionado para mostrarlo (en lugar de solo filtrar por él) es una búsqueda aparte, a mano, mediante los propios métodos de db (db.get_event_from_handle(...), etc.), siguiendo la misma indirección por handles que usa internamente el modelo de datos de Gramps.

Las colecciones también funcionan: any(c.given_name == 'Steve' for c in children) coincide con una familia si algún hijo cumple una condición, y len([c for c in children if c.gender == Person.MALE]) > 1 cuenta solo los hijos varones. Consulte GOQL para ver todo lo relativo a las colecciones, incluido su límite actual (un salto dentro de una colección, sin seguir encadenando más allá de lo que contiene).

Reaccionar al registro seleccionado

record = get_selected()
if record is not None:
    if isinstance(record, Person):
        ...

get_selected() devuelve el registro que esté abierto en ese momento en el panel de detalles de la vista: None cuando no hay nada seleccionado, o cuando se ejecuta desde la vista previa del editor independiente (que no tiene ningún contexto de vista). Hace una petición de red en la primera llamada de una ejecución y la memoriza para el resto. isinstance(record, Person) (etc.) le indica qué tipo de registro ha obtenido, lo cual importa en un Gramplet asociado a «Todas» las vistas. Para que se actualice en vivo mientras hace clic por la lista, active «Re-run automatically when the selected record changes» en el propio editor del Gramplet; si lo deja desactivado (por defecto), get_selected() sigue funcionando, pero solo refleja lo que estuviera seleccionado la última vez que el Gramplet se ejecutó por algún otro motivo.

Reaccionar al filtro activo

Un Gramplet puede añadir su propia consulta sobre el filtro que esté aplicado en ese momento a la vista (escrito en el propio cuadro de búsqueda de la vista o aplicado mediante el selector «Filtros»: un filtro guardado o una regla personalizada), de modo que filtrar la lista hasta una rama del árbol restringe la salida del Gramplet de la misma manera:

rows = filter(
    "person",
    where=and_filters(get_filter(), "birth.date.sortval is not None and death.date.sortval is not None"),
)

Esto necesita views: [...] en el manifiesto (get_filter() solo devuelve un where_expr que corresponda al tipo de objeto de la propia vista) y listensToFilter: true para que el Gramplet realmente se vuelva a mostrar cuando cambie el filtro.

Diagramas

pygal, matplotlib y plotly vienen incluidos: un simple import (o, para plotly, from plotly.subplots import make_subplots) funciona sin conexión, sin necesidad de %pip install. print(fig) sobre una Figure de plotly, una cadena SVG de pygal o una figura de matplotlib se reconoce automáticamente y se muestra como diagrama, sin necesidad de to_html()/incrustación manual.

Para los datos de un diagrama, pase what=[...] cuando solo necesite uno o dos campos: people() por sí solo descarga todos los campos de cada registro, lo cual es un desperdicio para un diagrama que solo representa, por ejemplo, la edad al fallecer.

Disposición lado a lado

col1, col2 = st.columns([2, 1])
with col1:
    row("Wider column")
with col2:
    row("Narrower column")

st.columns(spec) dispone spec regiones lado a lado (un entero para ese número de columnas de igual ancho, o una lista de pesos (st.columns([2, 1])) para anchos proporcionales) y devuelve una región por columna. Todo lo que se escriba dentro de with col: (row()/html()/print(), u otra llamada st.*, incluido un st.columns() anidado) va a esa columna en lugar de al nivel superior; col.write(x) funciona igual sin un bloque with.

Un Gramplet en su propia ventana

Gramplets → Nuevo Gramplet… abre el editor de Gramplets preparado para esta clase (en uno existente, ponga Clase en Gramplet — se abre en una ventana desde el menú Gramplets). En lugar de los ajustes de Gramplet de vista hay un campo Categoría, que lo agrupa con otros en el menú Gramplets (p. ej. chart); si se deja en blanco, va a Otros.

El código se ejecuta en cuanto se abre la ventana. Para pedir datos primero, dibújelos con los widgets st.* y haga el trabajo real detrás de un st.button. Siguen los nombres y argumentos de Streamlit: st.button, st.text_input, st.number_input, st.checkbox, st.selectbox, st.radio, st.columns, st.progress, los cuadros de mensaje st.info / st.success / st.warning / st.error, y st.session_state. Funcionan igual en los Gramplets de vista. Para un Gramplet de larga duración, muestre una barra con st.progress(value, text), como en Streamlit: value va de 0 a 100 (o de 0.0 a 1.0), y el .progress(...) de la barra devuelta la mueve en su sitio y .empty() la quita; es lo bastante barato como para llamarlo una vez por fila. También funciona en los Gramplets de vista. people(..., limit=None) (y las demás funciones de lista) devuelven todas las coincidencias en lugar de las 50 primeras:

surname = st.text_input("Surname", "Smith")
if st.button("Run"):
    everyone = people(f"primary_name.surname_list[0].surname == '{surname}'", limit=None)
    bar = st.progress(0, text="Checking people...")
    for i, person in enumerate(everyone):
        bar.progress((i + 1) / len(everyone))
        ...
    bar.empty()

Instalar un paquete de terceros

%pip install unidecode
from unidecode import unidecode

%pip install es sintaxis «mágica» de Jupyter, que se reescribe como una llamada real await micropip.install([...]) antes de que se ejecute su código: debe ir en su propia línea, al principio, exactamente como en un cuaderno (escribirlo dentro de un if, por ejemplo, no funcionará). Solo funciona con paquetes puramente de Python, sin código compilado ni extensiones en C; pygal/matplotlib/plotly no lo necesitan porque vienen incluidos, pero la mayoría de las pequeñas utilidades puramente de Python de PyPI se instalan bien así.

Cambiar datos

Un Gramplet (no un Gramplet de vista: esos nunca cambian datos) puede cambiar y eliminar registros una vez que se activa Permitir cambios en los datos en el editor de Gramplets. La API es la que usa Gram.py Script en Gramps de escritorio: cambie un registro como cambiaría cualquier objeto de Python.

for person in people("len(citation_list) == 0", limit=None):
    person.private = True

Mark Unsourced People Private, de la tienda, es un ejemplo completo: primero encuentra a las personas de forma barata con filter() y después obtiene y cambia solo las que lo necesitan, dentro de un grupo begin_changes() con nombre. Tag People muestra cómo añadir a una lista (person.tag_list.append(tag.handle): los registros guardan las etiquetas como handles, no como nombres) y un st.selectbox() en el que debe elegir antes de que cambie nada.

  • record.field = value cambia un campo, también los anidados (person.primary_name.first_name = "Chris"). Lo mismo hacen las ediciones de listas (.append(), .pop(), ...) y cualquier método set_*(), p. ej. person.set_privacy(True).
  • delete(record) elimina un registro. No quita las referencias a él desde otros registros.
  • begin_changes("message") / end_changes() agrupan los cambios intermedios bajo ese mensaje. Es opcional: sin ellos, toda la ejecución es un grupo, con el nombre del Gramplet.

Se pueden cambiar los registros de people() y las demás funciones en plural, get_raw_object(), get_object(), get_selected() y db.*. Los resultados de filter() no, ya que solo contienen algunos campos del registro. Pedir el mismo registro dos veces en una ejecución devuelve el mismo objeto, con sus cambios incluidos.

Primero, una prueba. En el editor de Gramplets toda ejecución es una prueba, y abrir la ventana del Gramplet desde el menú Gramplets también hace una prueba. No se guarda nada; bajo la salida, un cuadro amarillo Prueba enumera cada registro que cambiaría, con el valor antiguo y el nuevo de cada campo cambiado, y cada registro que se eliminaría, con cuántos otros registros todavía hacen referencia a él. Cada registro cambiado se comprueba primero contra su esquema de Gramps, la misma comprobación que hace el servidor; un valor no válido detiene la ejecución e indica el campo. Una forma en que una prueba puede diferir de la ejecución real: las búsquedas (filter(), count(), where=) se ejecutan en el servidor, así que siguen viendo el árbol tal como era antes de los cambios de la ejecución.

Después, Ejecutar. La ventana del Gramplet tiene botones Prueba y Ejecutar (el menú marca esos Gramplets con ✎). Ejecutar guarda cada grupo de cambios cuando el código lo cierra (en end_changes() o al terminar la ejecución) como una única transacción, de modo que aparece como una sola entrada en el historial del árbol. Mientras se muestra el resultado de una ejecución real, la barra de título dice Guardando cambios, y el cuadro bajo la salida se vuelve verde y marca cada grupo como guardado. Si el servidor rechaza un grupo (probablemente porque otra persona cambió uno de sus registros después de que el Gramplet lo leyera), no se guarda nada de ese grupo, la ejecución se detiene con un error y los grupos guardados antes siguen guardados. Los widgets de la salida (st.button("Apply"), etc.) vuelven a ejecutar el código en el mismo modo del que procede el resultado actual, así que un Gramplet puede mostrar lo que cambiaría en una prueba y aplicarlo tras Ejecutar.

Deshacer. Tras una ejecución real, la ventana muestra cuántos grupos guardó y un botón Deshacer, que los revierte, empezando por el último. Deshacer rechaza un grupo cuyos registros alguien haya cambiado desde entonces, en lugar de sobrescribir esas ediciones. Solo se ofrece para el resultado actual de la ventana: volver a ejecutar el Gramplet, o cerrar su ventana, lo da por terminado.

Publicar en la tienda

Una carpeta por Gramplet en gramplet-store/, con el nombre de su id (el nombre de la carpeta y el campo id del manifiesto deben coincidir):

gramplet-store/
  <slug>/
    manifest.json   # obligatorio
    code.py         # obligatorio -- el código fuente Python del Gramplet
    icon.png        # opcional (png/jpg/jpeg/svg/webp)

Campos de manifest.json:

Campo Obligatorio Significado
id sí Debe coincidir con el nombre de la carpeta.
name sí Se muestra como título de la entrada en la tienda.
description sí Se muestra bajo el nombre en la tienda.
version sí Semver simple, p. ej. "1.0.0"; increméntela cada vez que cambie code.py o el manifiesto, para que las copias instaladas puedan detectar que hay una actualización disponible.
author sí Se muestra en la tienda.
category sí Una etiqueta corta ("example", "detail", "utility", "chart", ...) que se usa para agrupar/filtrar entradas.
views no A qué vistas de tipo de objeto ("person", "family", ...) se puede añadir este Gramplet. Omítalo para «todos los tipos».
listensToSelection no Si este Gramplet debe volver a ejecutarse cuando cambie el registro seleccionado.
listensToFilter no Si este Gramplet debe volver a ejecutarse cuando cambie el filtro activo (cuadro de búsqueda o selector Filtros).
kind no "view" (el valor predeterminado) para un Gramplet de vista; "window" para un Gramplet que se ejecuta desde el menú Gramplets. views/listensTo* solo se aplican a los Gramplets de vista.
allowChanges no true para instalarlo con Permitir cambios en los datos activado (la tienda muestra una insignia Cambia sus datos). Solo se permite con "kind": "window".

Después de añadir o editar una entrada, regenere catalog.json, el único archivo que la aplicación obtiene realmente:

npm --prefix app run build:gramplet-catalog

Esto valida cada entrada (campos obligatorios presentes, id igual a su carpeta, version con aspecto de semver, code.py no vacío, kind válido) y falla de forma ruidosa en el primer problema en lugar de publicar un catálogo roto.

Lo que los Gramplets todavía no pueden hacer

Los Gramplets pueden cambiar y eliminar registros (véase Cambiar datos), pero todavía no pueden crear otros nuevos. Tampoco hay todavía un equivalente para otros tipos de complementos de Gramps (herramientas, informes). Consulte Hoja de ruta y limitaciones conocidas para ver en qué punto están.

⚠️ **GitHub.com Fallback** ⚠️