Skip to content

Gramplets.fr

Doug Blank edited this page Sep 22, 2026 · 1 revision

🌐 English · Deutsch

Gramplets et la boutique d'extensions

Les Gramplets sont le système d'extensions de Gramps Connect : de petits programmes Python, attachés à une vue (Individu, Famille, ou tout autre type d'objet — ou « Toutes ») qui interrogent l'arbre et affichent un résultat — un tableau, un diagramme, du texte brut, ou du HTML. Ce qui les rend inhabituels, c'est ils s'exécutent : chacun est du vrai Python, s'exécutant directement dans votre onglet de navigateur sous Pyodide (CPython compilé en WebAssembly), contre des wheels construits localement du propre modèle de données gramps.gen.lib de Gramps. Rien n'est installé sur votre machine, et rien ne s'exécute sur un serveur — voir Architecture.

Cela veut aussi dire que chaque Gramplet est modifiable. En ouvrir un depuis l'éditeur de Gramplets de l'application, et vous regardez (et pouvez changer) sa véritable source — pas une boîte noire derrière un panneau de réglages. Changer l'apparence d'un diagramme, ajuster ce qu'une recherche cherche, ou en écrire une nouvelle de zéro, tout cela sans quitter l'application.

Parcourir et installer

La boutique de Gramplets est un catalogue consultable à l'intérieur de l'application. Installer, mettre à jour, ou retirer n'importe quelle entrée en un clic. Elle est publiée indépendamment des propres versions de Gramps Connect — le catalogue est du contenu statique (gramplet-store/ dans le dépôt) récupéré par l'application en cours d'exécution au moment voulu, pas intégré au build de l'application, de nouveaux Gramplets peuvent donc apparaître dans la boutique sans que personne ne mette à jour Gramps Connect lui-même.

Des exemples déjà présents dans le catalogue incluent :

Gramplet Ce qu'il fait
Age-at-Death Histogram Histogramme plotly interactif de l'âge au décès sur tout l'arbre (ou la liste actuellement filtrée)
All Relationships Calculs de relations sur tout l'arbre
Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes Panneaux de détail dans le style des Gramplets de bureau pour la fiche sélectionnée
Born In Recherche par lieu de naissance
Crossing Relationships Trouve les relations qui croisent les générations de façon inhabituelle
GOQL Query Taper une clause where GOQL brute, et choisir des expressions de champs comme colonnes
Hello Table L'exemple de départ minimal
Interactive Search Un Gramplet de recherche en direct, au fil de la frappe
People Explorer Un Gramplet plus riche pour parcourir les individus
Phonebook Surname Sort Tri des noms de famille insensible aux accents (par ex. « Müller » se trie à côté de « Muller »)
Relationship / Relationships Comment deux personnes sélectionnées sont apparentées
Selected Record Réagit à la fiche actuellement ouverte
To-Do / Todo Tracker Suit les tâches de recherche à faire sur tout l'arbre
Tree Statistics Un tableau de bord des comptes de fiches et pourcentages dérivés

Écrire un Gramplet

Le code d'un Gramplet s'exécute dans un bac à sable avec un ensemble de fonctions et d'objets déjà disponibles — aucun import nécessaire pour atteindre l'arbre. Le répertoire gramplet_examples/ du dépôt est un parcours numéroté à travers l'API, du plus simple au plus avancé ; la même référence est disponible depuis l'application elle-même via le bouton d'aide (i) « Écrire un Gramplet » dans l'éditeur de Gramplets.

Le plus petit Gramplet utile

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, order=None, limit=50) récupère les Individus correspondant à une requête, comme fiches complètes, en un seul appel — et il existe un families()/events()/places()/ repositories()/sources()/citations()/media()/notes()/ tags() pour chaque autre type d'objet, même signature, juste une table différente.
  • where= s'écrit en GOQLexactement la même syntaxe que le champ de recherche de n'importe quelle vue de liste, y compris atteindre à travers des relations (birth.date.sortval) même quand le champ filtré n'est pas un de ceux affichés.
  • Pas besoin de await bien que ce soit un véritable appel réseau sous le capot — il est inséré automatiquement.
  • row(*values) ajoute une ligne au tableau. Passer un objet Person/Event/Place/… entier (plutôt qu'un champ choisi à la main) le représente comme un lien cliquable qui montre déjà son nom/titre complet et son ID Gramps — cliquer dessus ouvre une fenêtre pour voir cette fiche en Liste, Carte, Graphe, ou Chronologie.

Chaque fiche renvoyée par people()/families()/etc. est un « DataDict » Gramps — un dict ordinaire des champs de l'objet, mais avec un accès par point en plus : person.primary_name.first_name est la même valeur que person["primary_name"]["first_name"]. Y recourir chaque fois qu'il faut calculer quelque chose à partir d'un champ ou montrer quelque chose que la propre représentation de row() ne fait pas apparaître. row(), html(), et print() peuvent chacun être appelés autant de fois que voulu, dans n'importe quel ordre — tout apparaît dans l'ordre où cela a été ajouté.

Compter à moindre coût sans télécharger l'arbre

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>() est un comptage économique sur tout l'arbre — le total revient dans un en-tête de réponse, aucune fiche n'est réellement téléchargée. db.get_number_of(object_type, where=...) est l'équivalent conditionnel, pour quand le comptage lui-même a besoin d'un filtre.

Atteindre à travers des relations dans une requête, et en ressortir pour l'affichage

Une condition where= peut atteindre à travers une relation pour filtrer dessus — l'événement de naissance d'une personne, le père d'une famille, le lieu d'un événement — même si people()/ families()/etc. renvoient toujours un objet entier, indépendant, sans moyen de redemander directement un champ lié :

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,
)

Récupérer un champ lié pour l'affichage (plutôt que simplement filtrer dessus) est une recherche séparée, à la main, via les propres méthodes de dbdb.get_event_from_handle(...), et ainsi de suite — suivant la même indirection par handle que le propre modèle de données de Gramps utilise en interne.

Les collections fonctionnent aussi : any(c.given_name == 'Steve' for c in children) correspond à une famille si un enfant quelconque satisfait une condition, et len([c for c in children if c.gender == Person.MALE]) > 1 compte seulement les fils. Voir GOQL pour l'histoire complète des collections, y compris sa limite actuelle (un saut dans une collection, pas d'enchaînement supplémentaire au-delà de ce qu'elle contient).

Réagir à la fiche sélectionnée

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

get_selected() renvoie la fiche actuellement ouverte dans le propre panneau de détail de la vue — None quand rien n'est sélectionné, ou en s'exécutant depuis l'aperçu de l'éditeur autonome (qui n'a aucun contexte de vue du tout). Un seul appel réseau au premier appel d'une exécution, puis mémorisé pour le reste. isinstance(record, Person) (etc.) indique quel type de fiche a été obtenu, ce qui importe pour un Gramplet attaché aux vues « Toutes ». Pour que cela se mette à jour en direct en cliquant à travers la liste, activer « Réexécuter automatiquement quand la fiche sélectionnée change » dans le propre éditeur du Gramplet — désactivé (par défaut), get_selected() fonctionne toujours, mais ne reflète que ce qui était sélectionné la dernière fois que le Gramplet s'est exécuté pour une autre raison.

Réagir au filtre actif

Un Gramplet peut superposer sa propre requête à tout filtre actuellement appliqué à la vue — soit tapé dans le propre champ de recherche de la vue, soit appliqué via le sélecteur « Filtres » (un filtre enregistré ou une Règle personnalisée) — filtrer la liste pour la réduire à une branche de l'arbre restreint donc la propre sortie du Gramplet de la même façon :

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

Cela nécessite views: [...] défini dans le manifeste (get_filter() ne renvoie qu'un where_expr correspondant au propre type d'objet de la vue) et listensToFilter: true pour que le Gramplet se réexécute réellement quand le filtre change.

Diagrammes

pygal, matplotlib et plotly sont tous préinstallés — un simple import (ou, pour plotly, from plotly.subplots import make_subplots) fonctionne hors ligne, aucun %pip install nécessaire. print(fig) sur une Figure plotly, une chaîne SVG de pygal, ou une figure matplotlib est reconnu automatiquement et rendu comme un diagramme — aucun to_html()/intégration manuel nécessaire.

Pour les données de diagramme, préférer filter(object_type, what=[...], where=..., limit=...) à people()/families()/etc. quand seuls un ou deux champs sont nécessaires — people() signifie un appel réseau d'objet complet par fiche, ce qui est gaspillé pour un diagramme qui ne trace, disons, que l'âge au décès.

Disposition côte à côte

col1, col2 = st.columns([2, 1])
with col1:
    row("Colonne plus large")
with col2:
    row("Colonne plus étroite")

st.columns(spec) dispose spec régions côte à côte — un entier pour ce nombre de colonnes de largeur égale, ou une liste de poids (st.columns([2, 1])) pour des largeurs proportionnelles — et renvoie une région par colonne. Tout ce qui est écrit à l'intérieur de with col: (row()/html()/print(), ou un autre appel st.*, y compris un st.columns() imbriqué) atterrit dans cette colonne plutôt qu'au niveau supérieur ; col.write(x) fonctionne de la même façon sans bloc with.

Installer un paquet tiers

%pip install unidecode
from unidecode import unidecode

%pip install est une syntaxe magique de Jupyter, réécrite en un véritable appel await micropip.install([...]) avant l'exécution du code — cela doit être sur sa propre ligne, tout en haut, exactement comme dans un notebook (l'écrire à l'intérieur d'un if, par exemple, ne fonctionnera pas). Cela ne fonctionne que pour les paquets Python pur, sans code compilé/extension C ; pygal/matplotlib/plotly n'en ont pas besoin car ils sont préinstallés, mais la plupart des petits utilitaires Python purs sur PyPI s'installent bien de cette façon.

Publier dans la boutique

Un dossier par Gramplet dans gramplet-store/, nommé d'après son id (le nom du dossier et le propre champ id du manifeste doivent correspondre) :

gramplet-store/
  <slug>/
    manifest.json   # requis
    code.py         # requis -- le code source Python du Gramplet
    icon.png        # optionnel (png/jpg/jpeg/svg/webp)

Champs de manifest.json :

Champ Requis Signification
id oui Doit correspondre au nom du dossier.
name oui Affiché comme titre de l'entrée dans la boutique.
description oui Affichée sous le nom dans la boutique.
version oui Semver simple, par ex. "1.0.0" — l'incrémenter à chaque changement de code.py ou du manifeste, pour que les copies installées puissent détecter qu'une mise à jour est disponible.
author oui Affiché dans la boutique.
category oui Une courte étiquette ("example", "detail", "utility", "chart", …) utilisée pour grouper/filtrer les entrées.
views non À quelles vues de type d'objet ("person", "family", …) ce Gramplet peut être ajouté. Omettre pour « tous les types ».
listensToSelection non Si ce Gramplet doit se réexécuter quand la fiche sélectionnée change.
listensToFilter non Si ce Gramplet doit se réexécuter quand le filtre actif change (champ de recherche ou sélecteur Filtres).

Après avoir ajouté ou modifié une entrée, régénérer catalog.json — le seul fichier que l'application récupère réellement :

npm --prefix app run build:gramplet-catalog

Cela valide chaque entrée (champs requis présents, id correspond à son dossier, version ressemble à du semver, code.py non vide) et échoue bruyamment au premier problème plutôt que de publier un catalogue défectueux.

Ce que les Gramplets ne peuvent pas encore faire

Les Gramplets sont en lecture seule contre l'arbre aujourd'hui — ils peuvent interroger (filter()/get_object()) mais pas modifier ou créer d'objets. Il n'y a pas non plus encore d'équivalent pour les autres types d'add-ons de Gramps (outils, rapports). Voir Feuille de route et limitations connues pour l'état actuel de tout cela.

Clone this wiki locally