-
Notifications
You must be signed in to change notification settings - Fork 0
Gramplets.de
🌐 English
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.
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 |
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.
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 gibtfamilies()/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
awaitnö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.
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.
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 db —
db.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).
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.
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.
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.
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.
%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.
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-catalogDas 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.
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.
Gramps Connect is part of the family of Gramps-based software.
Using the app
- Overview
- Installing
- Deploying
- Messaging
- GOQL (advanced search)
- Gramplets & Add-on Store
- Data Model & Editing
- FAQ
Building & contributing