Skip to content

Gramplets.zh_CN

Doug Blank edited this page Oct 12, 2026 · 5 revisions

🌐 English · Deutsch · Español · Français · Nederlands

Gramplet 与插件商店

Gramplet 是 Gramps Connect 的插件系统:用于查询家谱并呈现结果——表格、图表、纯文本或 HTML——的小型 Python 程序。它们分为两类:Gramplet,从 Gramplets 菜单在自己的窗口中运行;以及视图 Gramplet,位于某个列表(成员、家庭或任何其他对象类型)下方的面板中,跟随您在列表中的选择。它们的不寻常之处在于运行的位置:每个 Gramplet 都是真正的 Python,直接在您的浏览器标签页中通过 Pyodide(编译为 WebAssembly 的 CPython)执行,使用本地构建的 Gramps 自己的 gramps.gen.lib 数据模型 wheel。不需要在您的电脑上安装任何东西,也不在服务器上运行任何东西——见架构。

这也意味着每个 Gramplet 都是可编辑的。在应用的 Gramplet 编辑器中打开一个,您看到的(并且可以修改的)就是它真正的源代码——而不是藏在设置面板后面的黑盒。修改图表的样子、调整查询要搜索的内容,或者从头写一个新的,全都无需离开应用。

谁可以使用 Gramplet

任何登录到该家谱的人都可以运行其中的 Gramplet,无论是从 Gramplets 菜单还是在列表的“视图 Gramplet”面板中。只有家谱的所有者或站点管理员可以编写它们:创建一个、编辑其代码,或通过 Gramplet 商店安装、更新或移除一个。对其他所有人来说,商店、新建 Gramplet… 和编辑铅笔图标根本不会出现。

角色 运行 Gramplet 编写 / 安装 Gramplet
访客 ✓(私密的除外) —
成员 ✓ —
贡献者 ✓ —
编辑者 ✓ —
所有者 ✓ ✓
管理员 ✓ ✓

编写 Gramplet 需要比编辑者通常拥有的更多权限,因为它的代码会在每个使用者的浏览器中运行,所以修改它会影响所有人的家谱,而不只是您自己的编辑。Gramplet 存储为一个打上 Gramplet 标签的普通媒体对象,因此通常的隐私规则同样适用:如果它被标记为私密,访客就看不到它。

允许修改数据也是如此(见修改数据):只有所有者或管理员才能为某个 Gramplet 开启它。任何人都可以对这样的 Gramplet 进行试运行,试运行不保存任何内容;保存其修改需要编辑者或更高的权限(服务器自身的添加、编辑和删除权限),因此对访客、成员和贡献者来说,窗口中的运行按钮是禁用的。

浏览与安装

Gramplet 商店是应用内一个可浏览的目录——从 Gramplets → Gramplet 商店… 打开。它有两个标签页:Gramplet 和视图 Gramplet;点击一下即可安装、更新或移除任意条目。安装的 Gramplet 会立即出现在 Gramplets 菜单中(按类别分组);安装的视图 Gramplet 则通过列表“视图 Gramplet”面板中的 + 添加到该列表。它的发布独立于 Gramps Connect 自身的版本——目录是静态内容(仓库中的 gramplet-store/),由正在运行的应用在运行时获取,而不是打包进应用构建中,因此新的 Gramplet 可以出现在商店里,而无需任何人升级 Gramps Connect 本身。

目录中已有的示例包括这些从 Gramplets 菜单运行的 Gramplet:

Gramplet 功能
Age at Death by Decade 按出生或去世年代显示去世年龄中位数的图表
Mark Unsourced People Private 把所有完全没有来源的人标记为私密——它会修改数据,所以打开它时是一次试运行,列出哪些人会被修改,点击运行才会保存(见修改数据)
People Without Sources 本人及其所有事件都没有任何引用的人(带进度条的全家谱扫描)
Possible Duplicate People 同名且出生年份相差不多的人,供检查和合并
Tag People 给当前成员筛选器中的所有人添加一个您从列表中选择的标签——它会修改数据,所以在您点击运行之前都只是试运行

以及这些视图 Gramplet:

视图 Gramplet 功能
Age-at-Death Histogram 整棵家谱(或当前筛选后的列表)去世年龄的交互式 plotly 直方图
All Relationships 在整棵家谱中计算关系
Attributes / Backlinks / Children / Citations / Coordinates / Events / Gallery / Notes 针对所选记录、风格类似桌面版 Gramplet 的详情面板
Born In 按出生地查询
Crossing Relationships 找出以不寻常方式跨越世代的关系
GOQL Query 输入原始的 GOQL where 子句,并选择字段表达式作为列
Hello Table 最简单的入门示例
People Search 边输入边搜索成员的实时搜索,可按姓名、性别和去世日期筛选
Phonebook Surname Sort 忽略重音的姓氏排序(例如“Müller”排在“Muller”旁边)
Relationship / Relationships 两个选中的人是什么亲属关系
Selected Record 对当前打开的记录做出响应
To-Do / Todo Tracker 跟踪整棵家谱中的研究待办事项
Tree Statistics 显示记录数量及派生百分比的仪表板

在每个列表视图(成员、家庭、事件……)中,您添加到该视图的视图 Gramplet 会以标签页的形式出现在表格下方一个可折叠的视图 Gramplet 面板中。面板默认是折叠的,而且视图 Gramplet 只在面板展开时运行:展开它就会运行当前选中的标签页。一次会话中的第一次运行还会加载浏览器内的 Python 运行时,因此可能需要几秒钟;之后的运行就很快了。保持面板折叠不会有任何开销,因为在您打开它之前不会加载 Python。

在自己窗口中运行的 Gramplet

Gramplet(与视图 Gramplet 不同)不绑定任何列表。从 Gramplets 菜单中选择一个——当类别多于一个时,会按类别分组到子菜单中——它就会在一个浮动窗口中打开:

  • 打开即运行。 需要先输入内容(姓氏、世代数)的 Gramplet 会绘制自己的输入字段和一个运行按钮,使用的部件与视图 Gramplet 相同。
  • 会修改数据的 Gramplet(在菜单中以 ✎ 标记)打开时会先试运行,并带有试运行 / 运行按钮和撤销——见修改数据。
  • 它能看到后面的列表。 如果主视图中打开了一个列表,Gramplet 会看到该列表的所选记录和当前筛选器,即运行开始时的状态;之后导航到别处不会改变正在进行的运行。
  • 进度与取消。 运行时间较长的 Gramplet 在工作时会显示进度条;取消会立即停止它,并保留它到那时为止产生的内容。
  • 同时继续工作。 最小化窗口(▾)后,它会变成屏幕底部的一个小标签,显示进度;运行会继续进行。最小化的 Gramplet 完成时,您会收到一条带有显示按钮的通知。多个 Gramplet 可以同时运行(最多五个窗口),各自独立,并且在您切换视图时保持打开。
  • 移动、调整大小、最大化。 拖动标题栏移动窗口,拖动角落调整大小,或将其最大化(⤢,或双击标题)。每个 Gramplet 都会在您的浏览器中记住其窗口的大小和位置。
  • 编辑。 对于家谱所有者或站点管理员(见谁可以使用 Gramplet),窗口标题栏上有一个铅笔图标(✎),可打开 Gramplet 编辑器;保存后窗口会以您的修改重新启动。
  • 关闭。 要从头再次运行某个 Gramplet,请关闭其窗口,然后从菜单中重新选择它。关闭仍在运行的窗口前会先询问,离开或重新加载页面时也一样——关闭浏览器标签页会停止所有运行。

结果只显示在窗口中;目前还不会保存到任何地方。

编写 Gramplet

Gramplet 的代码在一个沙箱中运行,其中已经提供了一组函数和对象——访问家谱不需要任何 import。仓库中的 gramplet_examples/ 目录是一份带编号的 API 导览,从最简单到最高级;同样的参考资料也可以在应用内通过 Gramplet 编辑器中的 (i)“Writing a Gramplet”帮助按钮查看。

最小的实用 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) 向表格添加一行。传入一个完整的成员/事件/地点/……对象(而不是手动挑出的某个字段)时,它会被渲染为一个可点击的链接,已经显示其完整的姓名/标题和 Gramps ID——点击它会弹出一个窗口,可以在列表、地图、图表或年谱中查看该记录。
  • name_of(person) 以文本形式给出一个人的姓名——先名后姓(“Root Ancestor”),与 row() 的标注方式相同——可用于标题或句子。它也接受此人的某个姓名,例如 name_of(person.alternate_names[0])。

people()/families() 等返回的每条记录都是 Gramps 的“DataDict”——一个包含对象字段的普通 dict,但额外支持点号访问:person.primary_name.first_name 与 person["primary_name"]["first_name"] 是同一个值。每当您想根据某个字段计算些什么,或者显示 row() 自身的渲染不会呈现的字段时,就可以用它。row()、html() 和 print() 都可以按任意顺序调用任意多次——所有内容都会按您添加的顺序显示。

打印记录永远不会出错: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= 条件可以跨越一个关系来进行筛选——一个人的出生事件、一个家庭的父亲、一个事件的地点——尽管 people()/families() 等总是返回一个完整的、互不相关的对象,没有办法直接要求返回相关字段:

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

要取回一个相关字段用于显示(而不只是用它筛选),需要另外通过 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) 把 spec 个区域并排排列——传入整数表示相应数量的等宽列,传入权重列表(st.columns([2, 1]))表示按比例分配宽度——并为每一列返回一个区域。在 with col: 内写入的任何内容(row()/html()/print(),或另一个 st.* 调用,包括嵌套的 st.columns())都会落在该列中,而不是顶层;col.write(x) 的作用相同,只是不需要 with 块。

在自己窗口中运行的 Gramplet

Gramplets → 新建 Gramplet… 会打开为这一类准备好的 Gramplet 编辑器(对于已有的 Gramplet,把类型设为 Gramplet — 从 Gramplets 菜单在窗口中打开)。视图 Gramplet 的设置被一个类别字段取代,用于在 Gramplets 菜单中把它与其他 Gramplet 分组(例如 chart);留空则放在其他下。

窗口一打开代码就会运行。要先请求输入,请用 st.* 部件绘制输入项,并把真正的工作放在一个 st.button 后面。它们遵循 Streamlit 的名称和参数: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 中的用法相同。对于运行时间较长的 Gramplet,可以像在 Streamlit 中一样用 st.progress(value, text) 显示进度条:value 为 0–100(或 0.0–1.0),返回的进度条的 .progress(...) 会原地更新它,.empty() 则移除它——开销很小,足以每行调用一次。它在视图 Gramplet 中同样可用。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()

安装第三方包

%pip install unidecode
from unidecode import unidecode

%pip install 是 Jupyter 魔法命令语法,会在您的代码运行之前被改写为真正的 await micropip.install([...]) 调用——它必须独占一行,并且位于顶部,就像在笔记本中一样(例如把它写在 if 里面是行不通的)。这只适用于纯 Python 包,即没有编译代码/C 扩展的包;pygal/matplotlib/plotly 不需要这样做,因为它们已经预先打包,但 PyPI 上大多数小型纯 Python 工具都可以这样顺利安装。

修改数据

在 Gramplet 编辑器中开启允许修改数据后,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)——记录以句柄而不是名称存储标签),以及一个必须先做出选择、之后才会修改任何内容的 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 编辑器中,每次运行都是试运行;从 Gramplets 菜单打开 Gramplet 的窗口时也会进行一次试运行。不会保存任何内容;在输出下方,一个黄色的试运行框会列出每条将被修改的记录,以及每个被修改字段的旧值和新值,还有每条将被删除的记录,以及仍有多少其他记录引用它。每条被修改的记录都会先按其 Gramps 模式进行检查,与服务器所做的检查相同;无效的值会停止运行并指出该字段。试运行与真实运行可能存在一个差异:搜索(filter()、count()、where=)在服务器上运行,所以它们看到的仍然是本次运行修改之前的家谱。

然后运行。 Gramplet 的窗口有试运行和运行按钮(菜单以 ✎ 标记此类 Gramplet)。运行会在代码关闭每个修改组时——在 end_changes() 处,或运行结束时——将其作为一个事务保存,因此它在家谱历史中显示为一条记录。显示真实运行的结果时,标题栏会显示正在保存修改,输出下方的框会变成绿色,并把每个组标记为已保存。如果服务器拒绝某个组——最可能的原因是在 Gramplet 读取之后有别人修改了其中的某条记录——该组的任何内容都不会保存,运行会因错误而停止,而在此之前已保存的组仍然保持保存。输出中的部件(st.button("Apply") 等)会以当前结果所来自的模式重新运行代码,因此 Gramplet 可以在试运行中展示它将修改的内容,并在点击运行后应用这些修改。

撤销。 真实运行之后,窗口会显示保存了多少个组,以及一个撤销按钮,它会从最后一个开始依次撤回这些组。如果某个组的记录在此期间被别人修改过,撤销会拒绝处理该组,而不是覆盖那些编辑。它只针对窗口的当前结果提供:再次运行该 Gramplet,或关闭其窗口,都会使其失效。

发布到商店

每个 Gramplet 在 gramplet-store/ 中占一个文件夹,以其 id 命名(文件夹名必须与清单自身的 id 字段一致):

gramplet-store/
  <slug>/
    manifest.json   # 必需
    code.py         # 必需 -- Gramplet 的 Python 源代码
    icon.png        # 可选(png/jpg/jpeg/svg/webp)

manifest.json 字段:

字段 必需 含义
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 有效),并在遇到第一个问题时明确报错,而不是发布一个有问题的目录。

Gramplet 目前还做不到的事

Gramplet 可以修改和删除记录(见修改数据),但还不能创建新记录。对于其他类型的 Gramps 插件(工具、报告),也还没有对应的机制。这些方面的现状见路线图与已知限制。

Clone this wiki locally