Writing Gramplets.de - dsblank/gramps-connect GitHub Wiki

🌐 English · Español · Français · Nederlands · 简体中文

Gramplets schreiben

Wie Sie ein eigenes Gramplet schreiben und im Gramplet-Store veröffentlichen. Nur der Eigentümer eines Stammbaums oder ein Site-Admin kann Gramplets anlegen oder bearbeiten (siehe Wer Gramplets nutzen kann).

Erste Schritte

Der Code eines Gramplets läuft in einer Sandbox, in der bereits eine Reihe von Funktionen und Objekten verfügbar ist, sodass keine Importe nötig sind, um an den Stammbaum zu kommen.

Das Verzeichnis gramplet_examples/ im Repository ist ein nummerierter Rundgang durch die API, vom Einfachsten zum Fortgeschrittensten. Dieselbe Referenz gibt es in der App über die Hilfe-Schaltfläche (i) „Writing a Gramplet“ 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)

Datensätze abrufen

people(where=None, what=None, order=None, limit=50) ruft die zu einer Abfrage passenden Personen als vollständige Datensätze in einem Aufruf ab. Mit what=["gramps_id", "primary_name"] ruft es nur diese Felder ab.

Es gibt families() / events() / places() / repositories() / sources() / citations() / media() / notes() / tags() für jeden anderen Datensatztyp, mit derselben Signatur.

Die Abfrage schreiben

where= wird in GOQL geschrieben, derselben Syntax wie im Suchfeld jeder Liste. Es kann über Beziehungen hinweg greifen (birth.date.sortval), auch wenn das Feld, nach dem Sie filtern, keines ist, das Sie anzeigen.

Kein await nötig, obwohl das im Hintergrund ein echter Netzwerkaufruf ist. Es wird automatisch für Sie eingefügt.

Ergebnisse anzeigen

  • row(*values) fügt der Tabelle eine Zeile hinzu. Ein ganzes Person-/Event-/Place-/...-Objekt statt eines seiner Felder wird als anklickbarer Link mit vollem Namen oder Titel und Gramps-ID dargestellt. Ein Klick darauf öffnet ein Popup, um diesen Datensatz in Liste, Karte, Graph oder Zeitleiste anzusehen.
  • name_of(person) ist der Name einer Person als Text, Vorname dann Nachnamen („Root Ancestor“), so wie row() sie beschriftet, für eine Überschrift oder einen Satz. Es nimmt auch einen ihrer Namen, z. B. name_of(person.alternate_names[0]).
  • row(), html() und print() lassen sich jeweils beliebig oft und in beliebiger Reihenfolge aufrufen. Alles erscheint in der Reihenfolge, in der Sie es hinzugefügt haben.

Mit Datensätzen arbeiten

Jeder von people(), families() und den anderen zurückgegebene Datensatz ist ein Gramps-„DataDict“: ein schlichtes Dict der Felder des Datensatzes, mit Punktzugriff obendrauf. person.primary_name.first_name ist derselbe Wert wie person["primary_name"]["first_name"]. Nutzen Sie es, wann immer Sie etwas aus einem Feld berechnen oder eines zeigen möchten, das row() nicht zeigt.

Einen Datensatz ausgeben

Das Ausgeben eines Datensatzes schlägt nie fehl. print(person) oder f"{person}" zeigt einen ganzen Datensatz so, wie row() ihn beschriftet („Root Ancestor [I0001]“), ein Datum als Datum und alles andere (ein filter()-Ergebnis, person.primary_name) als seine schlichten Felder.

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 über den ganzen Stammbaum. Die Summe kommt in einem Antwort-Header zurück, sodass keine Datensätze heruntergeladen werden. db.get_number_of(object_type, where=...) ist dasselbe mit einem Filter.

Über Beziehungen hinweg greifen

In einer Abfrage

Eine where=-Bedingung kann über eine Beziehung hinweg greifen, um darauf zu filtern: das Geburtsereignis einer Person, der Vater einer Familie, der Ort eines Ereignisses.

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

Zur Anzeige

people(), families() und die anderen geben immer den Datensatz selbst zurück, ohne Möglichkeit, direkt nach einem Feld eines verbundenen Datensatzes zu fragen. Eines zur Anzeige zurückzubekommen ist eine eigene Abfrage über die Methoden von db, etwa db.get_event_from_handle(...), anhand derselben Handles, die Gramps' eigenes Datenmodell verwendet.

Sammlungen

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 mehr, einschließlich der aktuellen Grenze: ein Schritt in eine Sammlung hinein, keine weitere Verkettung über ihren Inhalt 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 im Detailbereich der Ansicht geöffnet ist. Es ist None, wenn nichts ausgewählt ist, oder beim Ausführen aus der Editor-Vorschau, die keine Ansicht hat. Es macht beim ersten Aufruf eines Laufs einen Netzwerkabruf und merkt sich das Ergebnis für den Rest des Laufs.

isinstance(record, Person) (usw.) sagt Ihnen, welche Art Datensatz Sie bekommen haben, was für ein Gramplet zählt, das an „Alle“ Ansichten angehängt ist.

Aktualisieren, wenn sich die Auswahl ändert

Damit das Gramplet sich live aktualisiert, während Sie durch die Liste klicken, schalten Sie in seinem Editor „Re-run automatically when the selected record changes“ ein. Ist das aus (Standard), funktioniert get_selected() trotzdem, spiegelt aber nur wider, was beim letzten Lauf des Gramplets ausgewählt war.

Auf den aktiven Filter reagieren

Ein Gramplet kann seine eigene Abfrage über den in der Ansicht angewendeten Filter legen, ob im Suchfeld eingetippt oder unter Filter gewählt (ein gespeicherter Filter oder eine eigene Regel). Filtern Sie die Liste auf einen Zweig des Stammbaums, wird auch die Ausgabe des Gramplets entsprechend eingeschränkt:

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

Dafür braucht es zwei Manifest-Einstellungen:

  • views: [...], da get_filter() nur einen where_expr zurückgibt, der zum eigenen Datensatztyp der Ansicht passt;
  • listensToFilter: true, damit das Gramplet neu läuft, wenn sich der Filter ändert.

Diagramme

pygal, matplotlib und plotly sind mitgeliefert, sodass ein schlichtes import (oder bei plotly from plotly.subplots import make_subplots) offline funktioniert, ohne %pip install.

print(fig) auf einer plotly-Figure, einem SVG-String aus pygal oder einer matplotlib-Figur wird erkannt und als Diagramm gezeichnet, ohne manuelles to_html() oder Einbetten.

Übergeben Sie für Diagrammdaten what=[...], wenn Sie nur ein oder zwei Felder brauchen. people() allein lädt jedes Feld jedes Datensatzes herunter, was für ein Diagramm, das etwa nur das Sterbealter darstellt, verschwenderisch ist.

Nebeneinander-Layout

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

st.columns(spec) legt 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. Es gibt einen Bereich je Spalte zurück.

Alles, was innerhalb von with col: geschrieben wird (row(), html(), print() oder ein weiterer st.*-Aufruf, einschließlich eines verschachtelten st.columns()), landet in dieser Spalte. col.write(x) macht dasselbe ohne with-Block.

Ein Gramplet in einem eigenen Fenster

Gramplets → Neues Gramplet… öffnet den Gramplet-Editor für diese Art. Bei einem bestehenden Gramplet setzen Sie Art auf Gramplet — opens in a window from the Gramplets menu.

Anstelle der Ansichts-Gramplet-Einstellungen gibt es ein Feld Kategorie, das es im Gramplets-Menü mit anderen gruppiert (z. B. chart). Leer gelassen, landet es unter Other.

Nach Eingaben fragen

Der Code läuft, sobald sich das Fenster öffnet. Um zuerst nach Eingaben zu fragen, zeichnen Sie diese mit den st.*-Widgets und erledigen Sie die eigentliche Arbeit hinter einem st.button. Sie folgen den Namen und Argumenten von Streamlit und funktionieren in Ansichts-Gramplets genauso:

  • st.button, st.text_input, st.number_input, st.checkbox, st.selectbox, st.radio
  • st.columns, st.progress
  • die Meldungskästen st.info / st.success / st.warning / st.error
  • st.session_state

Fortschritt anzeigen

Zeigen Sie bei einem lange laufenden Gramplet einen Balken mit st.progress(value, text), wie in Streamlit. value ist 0–100 (oder 0.0–1.0). .progress(...) des zurückgegebenen Balkens bewegt ihn an Ort und Stelle, und .empty() entfernt ihn, günstig genug für einen Aufruf je Zeile.

people(..., limit=None) (und die anderen Listenfunktionen) geben jeden Treffer zurück statt der ersten 50:

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

Lange Läufe

Ein Gramplet kann so lange laufen, wie es braucht, und eine stundenlang offene Seite kann sie trotzdem ausführen. Jede Anfrage eines Gramplets verwendet Ihre aktuelle Anmeldung, die wie im Rest der App im Hintergrund erneuert wird. Nur wenn die Anmeldung selbst abgelaufen ist, stoppt ein Lauf, mit Your session has expired. Sign in again, then rerun this Gramplet.

Ein Paket eines Drittanbieters installieren

%pip install unidecode
from unidecode import unidecode

%pip install ist Jupyter-Syntax, die vor dem Ausführen Ihres Codes in einen echten Aufruf await micropip.install([...]) umgeschrieben wird. Es muss in einer eigenen Zeile ganz oben stehen, wie in einem Notebook; innerhalb eines if zum Beispiel funktioniert es nicht.

Das funktioniert nur für Pakete aus reinem Python ohne kompilierten oder C-Erweiterungscode. Die meisten kleinen reinen Python-Hilfsmittel auf PyPI lassen sich so problemlos installieren.

Daten aus Code ändern

Ein Gramplet (kein Ansichts-Gramplet, das nie Daten ändert) kann Datensätze ändern und löschen, sobald im Gramplet-Editor Datenänderungen erlauben eingeschaltet ist. Die API ist dieselbe, die Gram.py Script in Gramps Desktop verwendet: Ändern Sie einen Datensatz wie jedes andere Python-Objekt.

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

Beispiele im Store

  • Mark Unsourced People Private findet Personen zuerst günstig mit filter() und ruft dann nur die ab, die es braucht, und ändert sie, innerhalb einer benannten begin_changes()-Gruppe.
  • Tag People fügt einer Liste etwas hinzu (person.tag_list.append(tag.handle); Datensätze speichern Etiketten als Handles, nicht als Namen) und verwendet ein st.selectbox(), in dem Sie wählen müssen, bevor sich etwas ändert.

Ändern, Löschen und Gruppieren

  • record.field = value ändert ein Feld, auch verschachtelte (person.primary_name.first_name = "Chris"). Ebenso Listenänderungen (.append(), .pop(), ...) und jede set_*()-Methode, z. B. person.set_privacy(True).
  • delete(record) löscht einen Datensatz. Verweise anderer Datensätze darauf werden nicht entfernt.
  • begin_changes("message") / end_changes() gruppieren die Änderungen dazwischen unter dieser Nachricht. Sie sind optional: Ohne sie ist der ganze Lauf eine Gruppe, benannt nach dem Gramplet.

Welche Datensätze sich ändern lassen

Datensätze aus people() und den anderen Pluralfunktionen, get_raw_object(), get_object(), get_selected() und db.* lassen sich ändern. filter()-Ergebnisse nicht, da sie nur einige Felder eines Datensatzes enthalten.

Wer in einem Lauf zweimal nach demselben Datensatz fragt, bekommt dasselbe Objekt zurück, mit Ihren Änderungen darin.

Probeläufe während der Entwicklung

Im Gramplet-Editor ist jeder Lauf ein Probelauf. Jeder geänderte Datensatz wird zuerst gegen sein Gramps-Schema geprüft, dieselbe Prüfung, die der Server macht; ein ungültiger Wert stoppt den Lauf und nennt das Feld.

Ein Probelauf kann sich in einem Punkt vom echten unterscheiden: Suchen (filter(), count(), where=) laufen auf dem Server und sehen daher den Stammbaum noch so, wie er vor den Änderungen des Laufs war.

Wann Änderungen gespeichert werden

In einem echten Lauf speichert Ausführen jede Gruppe, sobald der Code sie schließt, bei end_changes() oder am Ende des Laufs. Siehe Daten ändern dafür, was Benutzer sehen, einschließlich Rückgängig.

Widgets in der Ausgabe (st.button("Apply") und so weiter) führen den Code in dem Modus erneut aus, aus dem das aktuelle Ergebnis stammt. So kann ein Gramplet in einem Probelauf zeigen, was es ändern würde, und es nach Ausführen anwenden.

Im Store veröffentlichen

Ordneraufbau

Ein Ordner je Gramplet in gramplet-store/, benannt nach seiner id. Der Ordnername und das eigene id-Feld des Manifests müssen übereinstimmen.

gramplet-store/
  <slug>/
    manifest.json   # required
    code.py         # required -- the Gramplet's Python source
    icon.png        # optional (png/jpg/jpeg/svg/webp)

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).
kind nein "view" (Standard) für ein Ansichts-Gramplet; "window" für ein Gramplet, das aus dem Gramplets-Menü läuft. views/listensTo* gelten nur für Ansichts-Gramplets.
allowChanges nein true, um es mit eingeschaltetem Datenänderungen erlauben zu installieren (der Store zeigt ein Abzeichen Ändert Ihre Daten). Nur mit "kind": "window" erlaubt.

Den Katalog neu bauen

Erzeugen Sie nach dem Hinzufügen oder Bearbeiten eines Eintrags catalog.json neu, die einzige Datei, die die App abruft:

npm --prefix app run build:gramplet-catalog

Das prüft jeden Eintrag (Pflichtfelder vorhanden, id passt zu seinem Ordner, version sieht wie Semver aus, code.py nicht leer, kind gültig) und bricht beim ersten Problem deutlich ab, statt einen kaputten Katalog zu veröffentlichen.

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