Gramplets.es - dsblank/gramps-connect GitHub Wiki
🌐 English · Deutsch · Français · Nederlands · 简体中文
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.
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.
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.
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.
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.
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, conwhat=["gramps_id", "primary_name"], solo esos campos); y hay unfamilies()/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
awaitaunque 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 etiquetarow(), 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.
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.
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).
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.
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.
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.
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.
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()%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í.
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 = TrueMark 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 = valuecambia un campo, también los anidados (person.primary_name.first_name = "Chris"). Lo mismo hacen las ediciones de listas (.append(),.pop(), ...) y cualquier métodoset_*(), 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.
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-catalogEsto 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.
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.