Writing Gramplets.de - dsblank/gramps-connect GitHub Wiki
🌐 English · Español · Français · Nederlands · 简体中文
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).
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.
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) 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.
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.
-
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 wierow()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()undprint()lassen sich jeweils beliebig oft und in beliebiger Reihenfolge aufrufen. Alles erscheint in der Reihenfolge, in der Sie es hinzugefügt haben.
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.
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.
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.
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,
)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.
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.
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.
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.
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: [...], daget_filter()nur einenwhere_exprzurückgibt, der zum eigenen Datensatztyp der Ansicht passt; -
listensToFilter: true, damit das Gramplet neu läuft, wenn sich der Filter ändert.
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.
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.
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.
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
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()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.
%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.
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-
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 benanntenbegin_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 einst.selectbox(), in dem Sie wählen müssen, bevor sich etwas ändert.
-
record.field = valueändert ein Feld, auch verschachtelte (person.primary_name.first_name = "Chris"). Ebenso Listenänderungen (.append(),.pop(), ...) und jedeset_*()-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.
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.
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.
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.
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)
| 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. |
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-catalogDas 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.