Skip to content

Gramplets.de

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

🌐 English

Gramplets und der Add-on-Store

Gramplets sind das Add-on-System von Gramps Connect: kleine Python-Programme, an eine Ansicht angehängt (Person, Familie oder jeder andere Objekttyp — oder „Alle“), die den Stammbaum abfragen und ein Ergebnis darstellen — eine Tabelle, ein Diagramm, reinen Text oder HTML. Was sie ungewöhnlich macht, ist wo sie laufen: jedes ist echtes Python, das direkt im eigenen Browser-Tab unter Pyodide ausgeführt wird (nach WebAssembly kompiliertes CPython), gegen lokal gebaute Wheels von Gramps' eigenem Datenmodell gramps.gen.lib. Nichts wird auf dem eigenen Rechner installiert, und nichts läuft auf einem Server — siehe Architektur.

Das bedeutet auch, dass jedes Gramplet bearbeitbar ist. Eines aus dem Gramplet-Editor der App heraus öffnen, und man sieht dessen echten Quellcode (und kann ihn ändern) — keine Blackbox hinter einem Einstellungsbereich. Wie ein Diagramm aussieht ändern, wonach eine Abfrage sucht anpassen, oder eine ganz neue von Grund auf schreiben, alles ohne die App zu verlassen.

Durchsuchen und installieren

Der Gramplet-Store ist ein durchsuchbarer Katalog innerhalb der App. Jeden Eintrag mit einem Klick installieren, aktualisieren oder entfernen. Er wird unabhängig von den eigenen Releases von Gramps Connect veröffentlicht — der Katalog ist statischer Inhalt (gramplet-store/ im Repository), zur Laufzeit von der laufenden App abgerufen, nicht in den App-Build eingebunden, sodass neue Gramplets im Store erscheinen können, ohne dass jemand Gramps Connect selbst aktualisiert.

Beispiele, die bereits im Katalog stehen:

Gramplet Was es tut
Age-at-Death Histogram Interaktives Plotly-Histogramm des Sterbealters über den Stammbaum (oder die aktuell gefilterte Liste)
All Relationships Verwandtschaftsberechnungen über den ganzen Stammbaum
Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes Detailbereiche im Stil der Desktop-Gramplets für den ausgewählten Datensatz
Born In Nachschlagen nach Geburtsort
Crossing Relationships Findet Verwandtschaften, die Generationen auf ungewöhnliche Weise kreuzen
GOQL Query Eine rohe GOQL-where-Bedingung eintippen und Feldausdrücke als Spalten wählen
Hello Table Das minimale Einstiegsbeispiel
Interactive Search Ein live, während der Eingabe suchendes Gramplet
People Explorer Ein umfangreicheres Gramplet zum Durchsuchen von Personen
Phonebook Surname Sort Akzent-unabhängige Nachnamensortierung (z. B. sortiert „Müller“ neben „Muller“)
Relationship / Relationships Wie zwei ausgewählte Personen verwandt sind
Selected Record Reagiert auf den gerade geöffneten Datensatz
To-Do / Todo Tracker Verfolgt Forschungs-To-dos über den ganzen Stammbaum
Tree Statistics Ein Dashboard mit Datensatzzahlen und abgeleiteten Prozentwerten

Ein Gramplet schreiben

Der Code eines Gramplets läuft in einer Sandbox mit bereits verfügbaren Funktionen und Objekten — keine Imports nötig, um an den Stammbaum zu kommen. Das Verzeichnis gramplet_examples/ im Repository ist ein durchnummerierter Rundgang durch die API, vom Einfachsten zum Fortgeschrittensten; dieselbe Referenz ist auch aus der App selbst heraus verfügbar, über die Hilfe-Schaltfläche (i) „Ein Gramplet schreiben“ im Gramplet-Editor.

Das kleinste nützliche Gramplet

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) ruft Personen ab, die auf eine Abfrage passen, als vollständige Datensätze, in einem Aufruf — und es gibt families()/events()/places()/repositories()/ sources()/citations()/media()/notes()/tags() für jeden anderen Objekttyp, dieselbe Signatur, nur eine andere Tabelle.
  • where= wird in GOQL geschrieben — exakt derselben Syntax wie das Suchfeld jeder Listenansicht, einschließlich des Reichens über Beziehungen hinweg (birth.date.sortval), selbst wenn das gefilterte Feld nicht eines der angezeigten ist.
  • Kein await nötig, obwohl das im Hintergrund ein echter Netzwerkaufruf ist — es wird automatisch eingefügt.
  • row(*values) fügt eine Zeile zur Tabelle hinzu. Ein ganzes Person-/Event-/Place-/…-Objekt zu übergeben (statt eines von Hand ausgewählten Felds davon) stellt es als anklickbaren Link dar, der bereits den vollen Namen/Titel und die Gramps-ID zeigt — ein Klick darauf öffnet ein Popup, um diesen Datensatz in Liste, Karte, Diagramm oder Zeitleiste anzusehen.

Jeder von people()/families()/usw. zurückgegebene Datensatz ist ein Gramps-„DataDict“ — ein schlichtes Dict der Felder des Objekts, aber mit Punktzugriff obendrauf: person.primary_name.first_name ist derselbe Wert wie person["primary_name"]["first_name"]. Darauf zurückgreifen, wann immer etwas aus einem Feld berechnet werden soll oder etwas gezeigt werden soll, das die eigene Darstellung von row() nicht zeigt. row(), html() und print() lassen sich jeweils beliebig oft aufrufen, in beliebiger Reihenfolge — alles erscheint in der Reihenfolge, in der es hinzugefügt wurde.

Günstiges Zählen, ohne den Stammbaum herunterzuladen

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>() ist eine günstige Zählung des ganzen Stammbaums — die Gesamtzahl kommt in einem Antwort-Header zurück, es werden keine Datensätze tatsächlich heruntergeladen. db. get_number_of(object_type, where=...) ist das bedingte Äquivalent, für wenn die Zählung selbst einen Filter braucht.

Über Beziehungen hinweg abfragen und für die Anzeige wieder herausreichen

Eine where=-Bedingung kann über eine Beziehung reichen, um darauf zu filtern — das Geburtsereignis einer Person, der Vater einer Familie, der Ort eines Ereignisses — auch wenn people()/families()/usw. immer ein ganzes, unabhängiges Objekt zurückgeben, ohne Möglichkeit, ein verwandtes Feld direkt zurückzubekommen:

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

Ein verwandtes Feld für die Anzeige zurückzubekommen (statt nur darauf zu filtern) ist ein separates, von Hand durchgeführtes Nachschlagen über die eigenen Methoden von dbdb.get_event_from_handle(...) und so weiter —, das derselben Handle-Indirektion folgt, die Gramps' eigenes Datenmodell intern verwendet.

Sammlungen funktionieren auch: any(c.given_name == 'Steve' for c in children) trifft auf eine Familie zu, wenn irgendein Kind eine Bedingung erfüllt, und len([c for c in children if c.gender == Person.MALE]) > 1 zählt nur die Söhne. Siehe GOQL für die vollständige Geschichte zu Sammlungen, einschließlich ihrer aktuellen Grenze (ein Sprung in eine Sammlung hinein, kein weiteres Verketten über das darin Enthaltene hinaus).

Auf den ausgewählten Datensatz reagieren

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

get_selected() gibt den Datensatz zurück, der gerade im eigenen Detailbereich der Ansicht geöffnet ist — None, wenn nichts ausgewählt ist, oder wenn es aus der eigenständigen Editor-Vorschau heraus läuft (die überhaupt keinen Ansichtskontext hat). Ein Netzwerkabruf beim ersten Aufruf in einem Durchlauf, danach für den Rest davon zwischengespeichert. isinstance(record, Person) (usw.) sagt, welche Art von Datensatz man bekommen hat, was für ein an „Alle“-Ansichten angehängtes Gramplet wichtig ist. Damit sich das live aktualisiert, während man sich durch die Liste klickt, „Bei Änderung des ausgewählten Datensatzes automatisch neu ausführen“ im eigenen Editor des Gramplets einschalten — ausgeschaltet (der Standard) funktioniert get_selected() weiterhin, spiegelt aber nur wider, was beim letzten Mal ausgewählt war, als das Gramplet aus irgendeinem anderen Grund zufällig lief.

Auf den aktiven Filter reagieren

Ein Gramplet kann seine eigene Abfrage über den gerade auf die Ansicht angewendeten Filter legen — entweder ins eigene Suchfeld der Ansicht eingetippt oder über die „Filter“-Auswahl angewendet (ein gespeicherter Filter oder eine eigene Regel) —, sodass das Herunterfiltern der Liste auf einen Zweig des Stammbaums die eigene Ausgabe des Gramplets auf dieselbe Weise einschränkt:

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

Das braucht ein gesetztes views: [...] im Manifest (get_filter() gibt nur einen where_expr passend zum eigenen Objekttyp der Ansicht zurück) und listensToFilter: true, damit sich das Gramplet tatsächlich neu rendert, wenn sich der Filter ändert.

Diagramme

pygal, matplotlib und plotly sind alle vorgebündelt — ein schlichtes import (oder, für plotly, from plotly.subplots import make_subplots) funktioniert offline, kein %pip install nötig. print(fig) auf einer plotly-Figure, ein SVG-String von pygal, oder eine matplotlib-Figure wird automatisch erkannt und als Diagramm gerendert — kein manuelles to_html()/Einbetten nötig.

Für Diagrammdaten filter(object_type, what=[...], where=..., limit=...) gegenüber people()/families()/usw. bevorzugen, wenn nur ein oder zwei Felder gebraucht werden — people() bedeutet einen vollständigen Objekt-Netzwerkabruf pro Datensatz, was für ein Diagramm verschwenderisch ist, das nur, sagen wir, das Sterbealter plottet.

Nebeneinander-Layout

col1, col2 = st.columns([2, 1])
with col1:
    row("Breitere Spalte")
with col2:
    row("Schmalere Spalte")

st.columns(spec) legt spec Bereiche nebeneinander an — eine Ganzzahl für so viele gleich breite Spalten, oder eine Liste von Gewichten (st.columns([2, 1])) für proportionale Breiten — und gibt einen Bereich pro Spalte zurück. Alles, was innerhalb von with col: geschrieben wird (row()/html()/print(), oder ein anderer st.*-Aufruf, einschließlich eines verschachtelten st.columns()), landet in dieser Spalte statt auf oberster Ebene; col.write(x) funktioniert genauso, ohne einen with-Block.

Ein Fremdpaket installieren

%pip install unidecode
from unidecode import unidecode

%pip install ist Jupyter-Magic-Syntax, vor der Ausführung des eigenen Codes in einen echten Aufruf await micropip.install([...]) umgeschrieben — es muss in einer eigenen Zeile stehen, ganz oben, genau wie in einem Notebook (es zum Beispiel in ein if zu schreiben, funktioniert nicht). Das funktioniert nur für reine Python-Pakete ohne kompilierten Code/C-Erweiterungen; pygal/matplotlib/plotly brauchen es nicht, da sie vorgebündelt sind, aber die meisten kleinen, reinen Python-Hilfspakete auf PyPI installieren sich auf diese Weise problemlos.

Im Store veröffentlichen

Ein Ordner pro Gramplet in gramplet-store/, benannt nach der eigenen id (der Ordnername und das eigene id-Feld des Manifests müssen übereinstimmen):

gramplet-store/
  <slug>/
    manifest.json   # erforderlich
    code.py         # erforderlich -- der Python-Quellcode des Gramplets
    icon.png        # optional (png/jpg/jpeg/svg/webp)

Felder von manifest.json:

Feld Erforderlich Bedeutung
id ja Muss mit dem Ordnernamen übereinstimmen.
name ja Wird als Titel des Eintrags im Store angezeigt.
description ja Wird unter dem Namen im Store angezeigt.
version ja Schlichtes Semver, z. B. "1.0.0" — bei jeder Änderung von code.py oder des Manifests erhöhen, damit installierte Kopien erkennen können, dass ein Update verfügbar ist.
author ja Wird im Store angezeigt.
category ja Ein kurzes Etikett ("example", "detail", "utility", "chart", …), um Einträge zu gruppieren/filtern.
views nein Zu welchen Objekttyp-Ansichten ("person", "family", …) dieses Gramplet hinzugefügt werden kann. Weglassen für „jeder Typ“.
listensToSelection nein Ob dieses Gramplet neu ausgeführt werden soll, wenn sich der ausgewählte Datensatz ändert.
listensToFilter nein Ob dieses Gramplet neu ausgeführt werden soll, wenn sich der aktive Filter ändert (Suchfeld oder Filter-Auswahl).

Nach dem Hinzufügen oder Bearbeiten eines Eintrags catalog.json neu erzeugen — die einzige Datei, die die App tatsächlich abruft:

npm --prefix app run build:gramplet-catalog

Das validiert jeden Eintrag (erforderliche Felder vorhanden, id passt zu seinem Ordner, version sieht wie Semver aus, code.py nicht leer) und schlägt beim ersten Problem laut fehl, statt einen defekten Katalog zu veröffentlichen.

Was Gramplets noch nicht können

Gramplets sind gegenüber dem Stammbaum heute nur lesend — sie können abfragen (filter()/get_object()), aber keine Objekte bearbeiten oder anlegen. Es gibt auch noch kein Äquivalent für andere Gramps-Add-on-Typen (Werkzeuge, Berichte). Siehe Roadmap und bekannte Einschränkungen für den aktuellen Stand dazu.

Clone this wiki locally