Repository navigation
Writing Gramplets.zh_CN
🌐 English · Deutsch · Español · Français · Nederlands
如何编写您自己的 Gramplet 并将其发布到 Gramplet 商店。只有家谱所有者或站点管理员才能创建或编辑 Gramplet(见谁可以使用 Gramplet)。
Gramplet 的代码运行在一个沙箱中,其中已经提供了一组函数和对象,因此访问家谱无需任何 import。
仓库中的 gramplet_examples/ 目录是一套带编号的 API 教程,从最简单到最高级。应用内也提供同样的参考:Gramplet 编辑器中的 (i)“Writing a 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, what=None, order=None, limit=50) 一次调用就能以完整记录的形式获取匹配查询的人物。使用 what=["gramps_id", "primary_name"] 则只获取这些字段。
其他每种记录类型都有对应的 families() / events() / places() / repositories() / sources() / citations() / media() / notes() / tags(),签名相同。
where= 用 GOQL 编写,语法与任何列表的搜索框相同。它可以跨越关系(birth.date.sortval),即使您筛选的字段并不是您要显示的字段。
虽然底层是真实的网络调用,但无需 await,它会被自动插入。
-
row(*values)向表格添加一行。传入整个 Person/Event/Place/... 对象(而不是它的某个字段),会将其渲染为可点击的链接,显示其全名或标题以及 Gramps ID。点击会打开一个弹窗,在列表、地图、关系图或时间线中查看该记录。 -
name_of(person)以文本形式返回一个人的姓名,先名后姓(“Root Ancestor”),与row()的标注方式一致,适合用于标题或句子。它也接受此人的某个名字,例如name_of(person.alternate_names[0])。 -
row()、html()和print()都可以按任意顺序调用任意多次。所有内容按您添加的顺序显示。
people()、families() 等返回的每条记录都是一个 Gramps“DataDict”:一个包含记录字段的普通 dict,并额外支持点号访问。person.primary_name.first_name 与 person["primary_name"]["first_name"] 是同一个值。当您想根据某个字段计算内容,或显示 row() 不显示的字段时,就用它。
打印记录从不出错。print(person) 或 f"{person}" 会按 row() 的标注方式显示整条记录(“Root Ancestor [I0001]”),日期显示为日期,其他内容(filter() 的结果、person.primary_name)显示为其普通字段。
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>() 是对整棵家谱的低成本计数。总数通过响应头返回,因此不会下载任何记录。db.get_number_of(object_type, where=...) 则是带筛选条件的同类计数。
where= 条件可以跨越关系进行筛选:人物的出生事件、家庭的父亲、事件的地点。
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() 等总是返回记录本身,无法直接请求关联记录的字段。要获取关联记录用于显示,需要通过 db 自己的方法另行查找,例如 db.get_event_from_handle(...),沿用 Gramps 自身数据模型所用的 handle。
any(c.given_name == 'Steve' for c in children) 在任一孩子满足条件时匹配该家庭,len([c for c in children if c.gender == Person.MALE]) > 1 则只统计儿子。更多内容见 GOQL,包括当前的限制:只能深入集合一层,不能在其内容之后继续链式访问。
record = get_selected()
if record is not None:
if isinstance(record, Person):
...get_selected() 返回视图详情面板中打开的记录。没有选中任何内容,或从没有视图的编辑器预览中运行时,它为 None。它在一次运行中首次调用时发起一次网络请求,之后在该次运行的剩余时间里记住结果。
isinstance(record, Person)(等)告诉您拿到的是哪种记录,这对附加到“全部”视图的 Gramplet 很重要。
要让 Gramplet 在您点选列表时实时更新,请在其编辑器中开启“Re-run automatically when the selected record changes”。关闭时(默认),get_selected() 仍然可用,但只反映 Gramplet 上次运行时选中的内容。
Gramplet 可以在视图当前应用的筛选器之上叠加自己的查询,无论该筛选器是在搜索框中输入的,还是在“筛选器”中选择的(已保存的筛选器或自定义规则)。把列表筛选到家谱的某一支后,Gramplet 的输出也会以同样方式缩小范围:
rows = filter(
"person",
where=and_filters(get_filter(), "birth.date.sortval is not None and death.date.sortval is not None"),
)这需要两项清单设置:
-
views: [...],因为get_filter()只返回与视图自身记录类型匹配的where_expr; -
listensToFilter: true,以便筛选器变化时 Gramplet 重新运行。
pygal、matplotlib 和 plotly 均已内置,因此普通的 import(对 plotly 则是 from plotly.subplots import make_subplots)可以离线使用,无需 %pip install。
对 plotly 的 Figure、pygal 生成的 SVG 字符串或 matplotlib 图形执行 print(fig),会被自动识别并绘制为图表,无需手动 to_html() 或嵌入。
对于图表数据,如果只需要一两个字段,请传入 what=[...]。单独调用 people() 会下载每条记录的每个字段,对于只绘制(比如)去世年龄的图表来说很浪费。
col1, col2 = st.columns([2, 1])
with col1:
row("Wider column")
with col2:
row("Narrower column")st.columns(spec) 把区域并排排列:传入整数表示该数量的等宽列,传入权重列表(st.columns([2, 1]))则按比例分配宽度。它为每一列返回一个区域。
在 with col: 内写入的任何内容(row()、html()、print() 或其他 st.* 调用,包括嵌套的 st.columns())都会进入该列。col.write(x) 无需 with 块即可达到同样效果。
Gramplets → 新建 Gramplet… 会打开为这种类型准备好的 Gramplet 编辑器。对于已有的 Gramplet,把 类型 设为 Gramplet — opens in a window from the Gramplets menu。
视图 Gramplet 的设置位置上会改为显示一个 类别 字段,用于在 Gramplets 菜单中将其与其他 Gramplet 归为一组(例如 chart)。留空则归入 Other。
窗口一打开代码就会运行。要先请求输入,请用 st.* 控件绘制输入项,把实际工作放在 st.button 之后。这些控件遵循 Streamlit 的名称和参数,在视图 Gramplet 中用法相同:
-
st.button、st.text_input、st.number_input、st.checkbox、st.selectbox、st.radio -
st.columns、st.progress - 消息框
st.info/st.success/st.warning/st.error st.session_state
对于长时间运行的 Gramplet,可以像 Streamlit 一样用 st.progress(value, text) 显示进度条。value 为 0–100(或 0.0–1.0)。返回的进度条的 .progress(...) 会原地更新它,.empty() 会移除它,开销低到每行调用一次也没问题。
people(..., limit=None)(以及其他列表函数)会返回所有匹配项,而不是前 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()Gramplet 可以运行任意长的时间,打开了几个小时的页面也仍能运行它们。Gramplet 发出的每个请求都使用您当前的登录状态,并像应用的其他部分一样在后台续期。只有当登录本身已结束时,运行才会停止,并显示 Your session has expired. Sign in again, then rerun this Gramplet.
%pip install unidecode
from unidecode import unidecode%pip install 是 Jupyter 风格的语法,在代码运行前会被改写为真正的 await micropip.install([...]) 调用。它必须单独占一行并位于顶部,就像在 notebook 中一样;例如写在 if 里面就不起作用。
这只适用于 纯 Python 包,即不含编译代码或 C 扩展的包。PyPI 上大多数小型纯 Python 工具都可以这样顺利安装。
在 Gramplet 编辑器中开启 允许修改数据 后,Gramplet(视图 Gramplet 永远不修改数据)就可以修改和删除记录。其 API 与 Gramps 桌面版上 Gram.py Script 使用的相同:像修改任何 Python 对象一样修改记录。
for person in people("len(citation_list) == 0", limit=None):
person.private = True-
Mark Unsourced People Private 先用
filter()低成本地找出人物,然后只获取并修改需要修改的那些,全部放在一个命名的begin_changes()组中。 -
Tag People 演示了向列表添加内容(
person.tag_list.append(tag.handle);记录以 handle 而非名称存储标签),并使用一个必须先做出选择才会修改任何内容的st.selectbox()。
-
record.field = value修改一个字段,嵌套字段也可以(person.primary_name.first_name = "Chris")。列表操作(.append()、.pop()等)和任何set_*()方法也一样,例如person.set_privacy(True)。 -
delete(record)删除一条记录。它不会移除其他记录对它的引用。 -
begin_changes("message")/end_changes()把两者之间的修改归入以该消息命名的组。它们是可选的:不使用时,整次运行是一个组,以 Gramplet 的名称命名。
来自 people() 及其他复数形式函数、get_raw_object()、get_object()、get_selected() 和 db.* 的记录都可以修改。filter() 的结果不行,因为它们只包含记录的部分字段。
在一次运行中两次请求同一条记录,会得到同一个对象,其中包含您的修改。
在 Gramplet 编辑器中,每次运行都是试运行。每条被修改的记录都会先按其 Gramps 模式进行校验,与服务器所做的校验相同;无效的值会使运行停止并指明是哪个字段。
试运行与真实运行有一点可能不同:搜索(filter()、count()、where=)在服务器上执行,因此它们看到的仍是本次运行修改之前的家谱。
在真实运行中,运行 会在代码关闭每个组时保存该组,即在 end_changes() 处或运行结束时。用户看到的内容(包括撤销)见修改数据。
输出中的控件(st.button("Apply") 等)会以当前结果所来自的模式重新运行代码。因此,Gramplet 可以在试运行中展示它将修改的内容,并在“运行”之后应用这些修改。
在 gramplet-store/ 中,每个 Gramplet 一个文件夹,以其 id 命名。文件夹名称必须与清单自身的 id 字段一致。
gramplet-store/
<slug>/
manifest.json # required
code.py # required -- the Gramplet's Python source
icon.png # optional (png/jpg/jpeg/svg/webp)
| 字段 | 必需 | 含义 |
|---|---|---|
id |
是 | 必须与文件夹名一致。 |
name |
是 | 作为该条目在商店中的标题显示。 |
description |
是 | 显示在商店中名称的下方。 |
version |
是 | 普通的 semver,例如 "1.0.0"——每当 code.py 或清单发生变化时都要递增,以便已安装的副本能检测到有可用更新。 |
author |
是 | 在商店中显示。 |
category |
是 | 一个简短标签("example"、"detail"、"utility"、"chart"……),用于对条目分组/筛选。 |
views |
否 | 该 Gramplet 可以添加到哪些对象类型视图("person"、"family"……)。省略表示“所有类型”。 |
listensToSelection |
否 | 所选记录变化时,该 Gramplet 是否应重新运行。 |
listensToFilter |
否 | 当前筛选器(搜索框或筛选器选择器)变化时,该 Gramplet 是否应重新运行。 |
kind |
否 |
"view"(默认)表示视图 Gramplet;"window" 表示从 Gramplets 菜单运行的 Gramplet。views/listensTo* 仅适用于视图 Gramplet。 |
allowChanges |
否 | 设为 true 则在安装时开启允许修改数据(商店会显示一个会修改您的数据徽章)。仅允许与 "kind": "window" 一起使用。 |
添加或编辑条目后,重新生成 catalog.json,这是应用唯一获取的文件:
npm --prefix app run build:gramplet-catalog它会校验每个条目(必填字段齐全、id 与文件夹一致、version 符合 semver、code.py 非空、kind 有效),并在遇到第一个问题时明确报错,而不是发布一个损坏的目录。
Gramps Connect is part of the family of Gramps-based software.
Getting started
Using the app
- Working Together
- Messaging
- Search & Filters
- GOQL (advanced search)
- Charts
- Maps
- Stories & Photos
- Data Model & Editing
- Gramplets & Add-on Store
- Administration & Settings
Building & contributing