28
edits
Changes
From Gramps
Synced from repo@41d611f via publish.py
<!--
Walkthroughs, one per addon kind, in order of increasing surface area.
Each tutorial:
- states the goal in one sentence
- shows the .gpr.py and the implementation module in full
- walks the new concepts (not every line — the explanatory lines)
- ends with "reload, restart, run" and what to expect
Cross-link out to 04-addon-kinds for kind-level details, 05-fundamentals
for cross-cutting concepts, 06-data-access for DB API details, and the
standalone wiki pages for deeper coverage.
Each tutorial assumes the reader has already followed 02-get-started
— so the development loop (where addons live, restart cycle) is not
re-explained.
-->
== Overview ==
End-to-end walkthroughs that take an author from empty folder to working addon. Each tutorial picks one kind, covers registration, implementation, and the reload cycle, and points at the conventions used to test it.
Read these in order or skip to the one that matches what you're building — they're independent. They assume you've already followed [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]], so we don't re-explain the user plugin directory or the restart cycle.
{|
! Tutorial
! Kind
! What it shows
|-
| [[#a-live-gramplet|A live Gramplet]]
| <code>GRAMPLET</code>
| Reading the DB, refreshing on selection change, signal subscriptions
|-
| [[#a-simple-tool|A simple Tool]]
| <code>TOOL</code>
| The Tool / ToolOptions pair, opening a dialog, writing in a <code>DbTxn</code>
|-
| [[#a-text-report|A text Report]]
| <code>REPORT</code>
| The Report / ReportOptions pair, the docgen abstraction, paragraph styles
|-
| [[#a-quick-view|A Quick View]]
| <code>QUICKVIEW</code>
| The <code>run()</code> entry point, the Simple Access API, context-menu integration
|-
| [[#a-custom-filter-rule|A custom filter Rule]]
| <code>RULE</code>
| Subclassing the namespace Rule base, declaring <code>labels</code>, <code>apply_to_one</code>
|}
For the conceptual map, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development|01-overview]]. For the full inventory of addon kinds and their registration constants, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]].
=== A note on tutorial-style code ===
The implementation modules below show the smallest code that demonstrates each kind. Two things are deliberately omitted to keep the lesson in focus, and both are '''required''' for shipped addons:
* A '''GPL-2.0-or-later license header''' at the top of every <code>.py</code> file. Copy the header from any existing addon, or see [16-guidelines → Coding styleN-guidelines.md#coding-style).
* '''Type hints''' on public functions and methods (Python 3.10+ syntax — <code>X | None</code>, <code>list[X]</code>). The tutorials skip them for readability; production addons should include them per [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|16-guidelines → Coding styleN-guidelines.md#coding-style).
Both are CI-checked on gramps core PRs (Black formats around the license header; `mypy` verifies the type hints); addons-source doesn't gate on them today but the rules apply to addon code regardless.
## A live Gramplet
**Goal.** Build a sidebar Gramplet that reads the active person from the database and shows their direct events, refreshing whenever the active person changes or the database is updated.
The Hello Gramplet from [02-get-started]] was static text. This one is dynamic — it subscribes to signals and re-reads the DB on each update.
=== Layout ===
Two files in a new folder <code>PersonEvents/</code>:
<pre>PersonEvents/
├── PersonEvents.gpr.py
└── personevents.py</pre>
=== <code>PersonEvents/PersonEvents.gpr.py</code> ===
<syntaxhighlight lang="python">register(
GRAMPLET,
id="PersonEvents",
name=_("Person Events"),
description=_("Lists the active person's direct events."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="personevents.py",
gramplet="PersonEventsGramplet",
gramplet_title=_("Events"),
height=200,
expand=True,
)</syntaxhighlight>
<code>height</code> and <code>expand</code> are Gramplet-specific layout fields; the rest are the same registration shape introduced in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals#the-gprpy-registration-file|05-fundamentals → The `.gpr.py` registration file]].
=== <code>PersonEvents/personevents.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.plug import Gramplet
_ = glocale.get_addon_translator(__file__).gettext
class PersonEventsGramplet(Gramplet):
"""List the active person's direct events; refresh on changes."""
def init(self):
"""Build the static parts of the UI once."""
self.set_use_markup(True)
self.set_text(_("No active person."))
def db_changed(self):
"""Subscribe to DB signals each time the active DB changes."""
self.connect(self.dbstate.db, "person-update", self.update)
self.connect(self.dbstate.db, "person-delete", self.update)
self.connect(self.dbstate.db, "event-update", self.update)
def active_changed(self, handle):
"""Active person changed — re-render."""
self.update()
def main(self):
"""Pull events for the active person and render them."""
person_handle = self.get_active("Person")
if not person_handle:
self.set_text(_("No active person."))
return
person = self.dbstate.db.get_person_from_handle(person_handle)
if person is None:
self.set_text(_("Active person not found."))
return
lines = [f"<b>{person.gramps_id}</b>\n"]
for event_ref in person.get_event_ref_list():
event = self.dbstate.db.get_event_from_handle(event_ref.ref)
if event is None:
continue
date = event.get_date_object()
lines.append(f"{event.get_type()} {date}")
self.set_text("\n".join(lines))</syntaxhighlight>
=== What's new vs. Hello Gramplet ===
* '''<code>db_changed()</code>''' subscribes to DB signals. Using <code>self.connect(...)</code> (defined on <code>Gramplet</code>) instead of <code>self.dbstate.db.connect(...)</code> means Gramps tracks the subscription keys for you and disconnects them automatically when the gramplet closes or the DB swaps out. The forgotten-disconnect bug class is gone.
* '''<code>active_changed(handle)</code>''' is called by Gramps when the user selects a different person in the active view. The default does nothing; calling <code>self.update()</code> triggers a redraw.
* '''<code>get_active("Person")</code>''' returns the handle of the active person for the current view, or <code>None</code>. It honours navigation context — in a Place view it returns the active place, etc.
* '''<code>set_use_markup(True)</code>''' lets <code>set_text()</code> interpret Pango markup (<code><b></code>, <code><i></code>, …); see [[Gramplets_development#Textual_Output_Methods|Gramplet textual methods]].
=== Try it ===
Drop the folder into your user plugin directory (or symlink it if you're on Gramps 6.1+), restart Gramps, open a tree, and add the Gramplet from the sidebar menu. Click around different people — the displayed events should change with the selection.
For the API surface this tutorial used (handles, refs, <code>iter_*</code>, <code>commit_*</code>), see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. For the signal inventory, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals#signals-addons-reacting-to-changes|05-fundamentals → Signals]].
== A simple Tool ==
'''Goal.''' A menu-launched Tool that scans the database for people with no recorded birth date and shows the list in a dialog.
Tools differ from gramplets in two ways: they're invoked from the Tools menu (not always visible), and they always carry an Options class — even a tool with no options must register an empty <code>ToolOptions</code> subclass.
=== Layout ===
<pre>MissingBirthDates/
├── MissingBirthDates.gpr.py
└── missingbirthdates.py</pre>
=== <code>MissingBirthDates/MissingBirthDates.gpr.py</code> ===
<syntaxhighlight lang="python">register(
TOOL,
id="MissingBirthDates",
name=_("Missing Birth Dates"),
description=_("Lists people with no recorded birth date."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="missingbirthdates.py",
category=TOOL_ANAL,
toolclass="MissingBirthDates",
optionclass="MissingBirthDatesOptions",
tool_modes=[TOOL_MODE_GUI],
)</syntaxhighlight>
<code>category=TOOL_ANAL</code> puts the tool under ''Tools → Analysis and Exploration''. Other categories (<code>TOOL_DBPROC</code>, <code>TOOL_DBFIX</code>, …) are listed in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds#tool|04-addon-kinds → `TOOL`]].
=== <code>MissingBirthDates/missingbirthdates.py</code> ===
<syntaxhighlight lang="python">from gi.repository import Gtk
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gui.dialog import OkDialog
from gramps.gui.plug import tool
_ = glocale.get_addon_translator(__file__).gettext
class MissingBirthDates(tool.Tool):
"""Scan the DB and report people with no recorded birth date."""
def __init__(self, dbstate, user, options_class, name, callback=None):
tool.Tool.__init__(self, dbstate, options_class, name)
db = dbstate.db
missing = []
for person in db.iter_people():
birth_ref = person.get_birth_ref()
if birth_ref is None:
missing.append(person)
continue
event = db.get_event_from_handle(birth_ref.ref)
if event is None or event.get_date_object().is_empty():
missing.append(person)
if not missing:
OkDialog(
_("Missing Birth Dates"),
_("Every person has a recorded birth date."),
parent=user.uistate.window,
)
return
lines = [f"{p.gramps_id}: {p.get_primary_name().get_name()}"
for p in missing]
OkDialog(
_("Missing Birth Dates"),
_("{n} people with no recorded birth date:\n\n{listing}").format(
n=len(missing),
listing="\n".join(lines),
),
parent=user.uistate.window,
)
class MissingBirthDatesOptions(tool.ToolOptions):
"""No options — placeholder required by the tool framework."""</syntaxhighlight>
=== What's new ===
* '''<code>tool.Tool.__init__(self, dbstate, options_class, name)</code>''' — the base-class constructor. The body of <code>__init__</code> is ''where the tool runs''; there's no separate <code>run()</code> method for GUI tools.
* '''<code>MissingBirthDatesOptions</code>''' is required even though we have no options. The <code>register(...)</code> call names it via <code>optionclass</code>, and Gramps would refuse to load the tool without it.
* '''<code>OkDialog</code>''' is the simplest modal report-back surface; for richer output, build a <code>Gtk.Dialog</code> directly (see <code>gramps/plugins/tool/dumpgenderstats.py</code> for the standard recipe).
=== Writing data ===
If your tool ''modifies'' the database, all writes go inside a <code>DbTxn</code>:
<syntaxhighlight lang="python">from gramps.gen.db import DbTxn
with DbTxn(_("Mark unreferenced media private"), db) as trans:
for media in db.iter_media():
if not db.find_backlink_handles(media.handle):
media.set_privacy(True)
db.commit_media(media, trans)</syntaxhighlight>
The transaction message is user-visible in the Undo history; translate it. See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access#mutating-data|06-data-access → Mutating data]] for the full pattern.
=== Try it ===
After restart, the tool appears in ''Tools → Analysis and Exploration → Missing Birth Dates''. Run it on <code>example.gramps</code> to see the dialog.
== A text Report ==
'''Goal.''' A simple text report that summarises the database — number of people, number of families, count by gender. Produces the same content through PDF, HTML, ODF, or any other docgen-supported format.
Reports are the heaviest of the everyday addon kinds. Three pieces work together:
* A '''Report''' class that knows how to walk the data and emit it as paragraphs and tables, leaving format details to the docgen.
* An '''Options''' class that defines user-adjustable options and the paragraph / font styles.
* A '''registration''' call wiring both into the menu.
=== Layout ===
<pre>DbSummary/
├── DbSummary.gpr.py
└── dbsummary.py</pre>
=== <code>DbSummary/DbSummary.gpr.py</code> ===
<syntaxhighlight lang="python">register(
REPORT,
id="DbSummary",
name=_("Database Summary"),
description=_("Produces a short summary of the family tree."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="dbsummary.py",
category=CATEGORY_TEXT,
require_active=False,
reportclass="DbSummaryReport",
optionclass="DbSummaryOptions",
report_modes=[REPORT_MODE_GUI, REPORT_MODE_CLI],
)</syntaxhighlight>
<code>category=CATEGORY_TEXT</code> makes this a text report — Gramps will offer the user the text-output document backends (PDF, ODF, plain text, …). <code>require_active=False</code> because a database summary doesn't need a specific active person.
=== <code>DbSummary/dbsummary.py</code> ===
<syntaxhighlight lang="python">from collections import Counter
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.lib import Person
from gramps.gen.plug import docgen
from gramps.gen.plug.report import MenuReportOptions, Report
from gramps.gen.plug.report import stdoptions
_ = glocale.get_addon_translator(__file__).gettext
class DbSummaryReport(Report):
"""A text report summarising the database."""
def __init__(self, database, options_class, user):
Report.__init__(self, database, options_class, user)
self.set_locale(
options_class.menu.get_option_by_name("trans").get_value()
)
self._count()
def _count(self):
"""Walk every Person and tally."""
self.total = 0
gender_counts = Counter()
surnames = Counter()
for person in self.database.iter_people():
self.total += 1
gender_counts[person.get_gender()] += 1
primary = person.get_primary_name()
surnames[primary.get_primary_surname().get_surname()] += 1
self.gender_counts = gender_counts
self.unique_surnames = len(surnames)
self.top_surname = (
surnames.most_common(1)[0] if surnames else (_("(none)"), 0)
)
def write_report(self):
"""Emit paragraphs into self.doc."""
self.doc.start_paragraph("DBS-Title")
self.doc.write_text(self._("Database Summary"))
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Total persons: {n}").format(n=self.total))
self.doc.end_paragraph()
for gender_code, label in [
(Person.MALE, _("Males")),
(Person.FEMALE, _("Females")),
(Person.UNKNOWN, _("Unknown gender")),
]:
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("{label}: {n}").format(
label=label,
n=self.gender_counts.get(gender_code, 0),
)
)
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Unique surnames: {n}").format(n=self.unique_surnames)
)
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Most common surname: {name} ({n})").format(
name=self.top_surname[0], n=self.top_surname[1])
)
self.doc.end_paragraph()
class DbSummaryOptions(MenuReportOptions):
"""Options form and default styles for DbSummaryReport."""
def add_menu_options(self, menu):
category = _("Report Options")
stdoptions.add_localization_option(menu, category)
def make_default_style(self, default_style):
# Title style: 18 pt bold sans-serif, centred, header level 1.
font = docgen.FontStyle()
font.set_size(18)
font.set_type_face(docgen.FONT_SANS_SERIF)
font.set_bold(True)
para = docgen.ParagraphStyle()
para.set_header_level(1)
para.set_alignment(docgen.PARA_ALIGN_CENTER)
para.set_font(font)
para.set_description(_("Style used for the title of the report."))
default_style.add_paragraph_style("DBS-Title", para)
# Body style: 12 pt serif.
font = docgen.FontStyle()
font.set_size(12)
font.set_type_face(docgen.FONT_SERIF)
para = docgen.ParagraphStyle()
para.set_font(font)
para.set_description(_("Style used for normal report text."))
default_style.add_paragraph_style("DBS-Normal", para)</syntaxhighlight>
=== What's new ===
* '''Two classes, one file.''' The <code>register()</code> call points <code>reportclass</code> at the Report and <code>optionclass</code> at the Options.
* '''<code>self.doc</code> is not a file.''' It's the live document — a docgen backend instance. The report writes paragraphs and text into it regardless of output format.
* '''Paragraph style names are prefixed.''' Use <code>DBS-</code> (or any short prefix unique to your report) on every style name. Reports get composed into Book reports, where every style name has to be unique across all contributing reports.
* '''Localisation is explicit.''' <code>stdoptions.add_localization_option</code> adds the standard "report locale" option to the form; the report reads it with <code>self.set_locale(...)</code> and uses <code>self._()</code> for strings that should follow the ''report's'' chosen locale rather than the UI locale. The leading underscore in <code>self._</code> is intentional.
* '''<code>MenuReportOptions</code>''' is the convenient base; for a no-options report, override only <code>add_menu_options</code> (to add the locale option) and <code>make_default_style</code> (to define paragraph styles).
=== Try it ===
After restart, the report appears in ''Reports → Text Reports → Database Summary''. Run it through any text document backend (PDF, ODF, plain text) to see the same content reformatted by each.
For more on the docgen abstraction, see [[Report_Generation|Report Generation]]. For richer reports (tables, multiple paragraph levels, graphical reports using <code>CATEGORY_DRAW</code>), see [[Report_API|Report API]].
== A Quick View ==
'''Goal.''' A right-click action on a person that lists their siblings — brothers and sisters from every family they're a child in.
Quick Views are the shortest path to a usable report. There's no class to subclass and no options form to maintain — just a <code>run()</code> function and the registration. They're written against the '''Simple Access API''' (<code>SimpleAccess</code>, <code>SimpleDoc</code>), which trades some power for very little code.
=== Layout ===
<pre>Siblings/
├── Siblings.gpr.py
└── siblings.py</pre>
=== <code>Siblings/Siblings.gpr.py</code> ===
<syntaxhighlight lang="python">register(
QUICKVIEW,
id="Siblings",
name=_("Siblings"),
description=_("Lists the active person's siblings."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="siblings.py",
category=CATEGORY_QR_PERSON,
runfunc="run",
)</syntaxhighlight>
<code>category=CATEGORY_QR_PERSON</code> puts the entry on the person context menu. <code>runfunc="run"</code> names the function Gramps calls. The full set of categories is listed in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds#quickview|04-addon-kinds → `QUICKVIEW`]].
=== <code>Siblings/siblings.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.simple import SimpleAccess, SimpleDoc
from gramps.gui.plug.quick import QuickTable
_ = glocale.get_addon_translator(__file__).gettext
def run(database, document, person):
"""Display all siblings of the given person."""
sdb = SimpleAccess(database)
sdoc = SimpleDoc(document)
sdoc.title(_("Siblings of {name}").format(name=sdb.name(person)))
sdoc.paragraph("")
table = QuickTable(sdb)
table.columns(_("Person"), _("Gender"), _("Birth date"))
own_gid = sdb.gid(person)
for family in sdb.child_in(person):
for child in sdb.children(family):
if sdb.gid(child) == own_gid:
continue
table.row(child, sdb.gender(child), sdb.birth_date(child))
document.has_data = True
table.write(sdoc)</syntaxhighlight>
=== What's new ===
* '''<code>run(database, document, person)</code>''' — the function signature is fixed by the QuickView kind. The third argument is the ''selected object'' of the category (<code>CATEGORY_QR_PERSON</code> → person, <code>CATEGORY_QR_FAMILY</code> → family, …).
* '''<code>SimpleAccess</code>''' is the high-level read interface — <code>sdb.children(family)</code>, <code>sdb.birth_date(person)</code>, <code>sdb.name(person)</code>. It hides handle dereferencing, refs, and date formatting. For the full surface, see [[Simple_Access_API|Simple Access API]].
* '''<code>SimpleDoc</code>''' is the matching write interface — <code>sdoc.title(...)</code>, <code>sdoc.paragraph(...)</code>, <code>sdoc.header1(...)</code>.
* '''<code>QuickTable</code>''' builds an interactive table where each row links back to a real Gramps object — clicking a person opens that person.
* '''<code>document.has_data = True</code>''' tells Gramps the report produced output. When all rows are filtered out, the empty-state path triggers instead.
=== Try it ===
After restart, right-click any person in the People view or the person editor. ''Quick View → Siblings'' appears in the menu. The result opens in a Quick View window; clicking a row in the table opens that person.
For Quick Views that don't fit the Simple Access surface, you can reach for the full DB API — see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. The two are complementary; a complex Quick View can use both.
== A custom filter Rule ==
'''Goal.''' A filter rule "Has at least N children" that the user can add to a custom person filter from the Filter Editor.
Filter rules are the smallest addon kind by line count and the one with the most reuse: a single rule, written once, drops into every filter the user composes — search, narrative website, reports, gramplets that accept a filter.
=== Layout ===
<pre>HasNChildren/
├── HasNChildren.gpr.py
└── hasnchildren.py</pre>
=== <code>HasNChildren/HasNChildren.gpr.py</code> ===
<syntaxhighlight lang="python">register(
RULE,
id="HasNChildren",
name=_("People with at least N children"),
description=_("Matches people who have at least N children."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="hasnchildren.py",
ruleclass="HasNChildren",
namespace="Person",
)</syntaxhighlight>
<code>namespace="Person"</code> says this rule applies to people. The other namespaces (<code>Family</code>, <code>Event</code>, <code>Place</code>, <code>Source</code>, <code>Citation</code>, <code>Repository</code>, <code>Media</code>, <code>Note</code>) get their own rules — Gramps' filter editor groups rules by namespace.
=== <code>HasNChildren/hasnchildren.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.filters.rules import Rule
_ = glocale.get_addon_translator(__file__).gettext
class HasNChildren(Rule):
"""Matches people with at least N children."""
labels = [_("Minimum count:")]
name = _("People with at least N children")
category = _("Family filters")
description = _("Matches people with at least N children")
def apply_to_one(self, db, person):
try:
minimum = int(self.list[0])
except (TypeError, ValueError):
return False
total = 0
for family_handle in person.get_family_handle_list():
family = db.get_family_from_handle(family_handle)
if family is None:
continue
total += len(family.get_child_ref_list())
if total >= minimum:
return True
return False</syntaxhighlight>
=== What's new ===
* '''<code>labels</code>''' declares the user-prompted arguments — one entry per text box in the filter-editor dialog. The user's typed values arrive on <code>self.list</code> in the same order. Always parse defensively; <code>self.list[0]</code> is a string straight from the GUI.
* '''<code>name</code>, <code>category</code>, <code>description</code>''' are class attributes — Gramps reads them off the class (no instance needed) when building the Add Rule dialog. <code>category</code> is the section the rule appears under in that dialog.
* '''<code>apply_to_one(self, db, person)</code>''' is the per-object hook. It returns <code>True</code> for a match, <code>False</code> for a non-match. Gramps calls it for every person in the namespace when applying the filter. On Gramps 6.0 the API is <code>apply_to_one</code>; older releases used <code>apply</code> (see [https://github.com/gramps-project/gramps/blob/maintenance/gramps60/gramps/gen/filters/rules/_rule.py#L162 gramps/gen/filters/rules/_rule.py:162]).
=== Optional hooks ===
* '''<code>prepare(self, db, user)</code>''' — called once before the rule is applied to many objects, on demand. Use it to precompute lookup tables when <code>apply_to_one</code> would otherwise repeat expensive work. Pair with <code>reset()</code> to release memory afterwards.
* '''<code>allow_regex = True</code>''' — opt the first label into regex input.
=== Try it ===
After restart, ''Edit → Person Filter Editor → Add → Add Rule'' shows "People with at least N children" under ''Family filters''. The user types a number in the "Minimum count" field; the rule does the rest.
The rule is also visible from gramplets like ''Filter Gramplet'' and as an input to any tool or report that accepts a person filter — no extra work needed; rules are uniform across the framework.
== See also ==
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]] — the prerequisites and the development loop these tutorials build on.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] — registration details per kind.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals|05-fundamentals]] — <code>.gpr.py</code> fields, signals, <code>requires_mod</code>, lifecycle hooks.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] — the DB API patterns used by these tutorials.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Testing|08-testing]] — how to test what you just wrote without launching Gramps.
* [[Report_API|Report API]], [[Report_Generation|Report Generation]] — depth on the docgen abstraction.
* [[Simple_Access_API|Simple Access API]] — the Quick View read surface.
{{stub}}
Walkthroughs, one per addon kind, in order of increasing surface area.
Each tutorial:
- states the goal in one sentence
- shows the .gpr.py and the implementation module in full
- walks the new concepts (not every line — the explanatory lines)
- ends with "reload, restart, run" and what to expect
Cross-link out to 04-addon-kinds for kind-level details, 05-fundamentals
for cross-cutting concepts, 06-data-access for DB API details, and the
standalone wiki pages for deeper coverage.
Each tutorial assumes the reader has already followed 02-get-started
— so the development loop (where addons live, restart cycle) is not
re-explained.
-->
== Overview ==
End-to-end walkthroughs that take an author from empty folder to working addon. Each tutorial picks one kind, covers registration, implementation, and the reload cycle, and points at the conventions used to test it.
Read these in order or skip to the one that matches what you're building — they're independent. They assume you've already followed [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]], so we don't re-explain the user plugin directory or the restart cycle.
{|
! Tutorial
! Kind
! What it shows
|-
| [[#a-live-gramplet|A live Gramplet]]
| <code>GRAMPLET</code>
| Reading the DB, refreshing on selection change, signal subscriptions
|-
| [[#a-simple-tool|A simple Tool]]
| <code>TOOL</code>
| The Tool / ToolOptions pair, opening a dialog, writing in a <code>DbTxn</code>
|-
| [[#a-text-report|A text Report]]
| <code>REPORT</code>
| The Report / ReportOptions pair, the docgen abstraction, paragraph styles
|-
| [[#a-quick-view|A Quick View]]
| <code>QUICKVIEW</code>
| The <code>run()</code> entry point, the Simple Access API, context-menu integration
|-
| [[#a-custom-filter-rule|A custom filter Rule]]
| <code>RULE</code>
| Subclassing the namespace Rule base, declaring <code>labels</code>, <code>apply_to_one</code>
|}
For the conceptual map, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development|01-overview]]. For the full inventory of addon kinds and their registration constants, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]].
=== A note on tutorial-style code ===
The implementation modules below show the smallest code that demonstrates each kind. Two things are deliberately omitted to keep the lesson in focus, and both are '''required''' for shipped addons:
* A '''GPL-2.0-or-later license header''' at the top of every <code>.py</code> file. Copy the header from any existing addon, or see [16-guidelines → Coding styleN-guidelines.md#coding-style).
* '''Type hints''' on public functions and methods (Python 3.10+ syntax — <code>X | None</code>, <code>list[X]</code>). The tutorials skip them for readability; production addons should include them per [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|16-guidelines → Coding styleN-guidelines.md#coding-style).
Both are CI-checked on gramps core PRs (Black formats around the license header; `mypy` verifies the type hints); addons-source doesn't gate on them today but the rules apply to addon code regardless.
## A live Gramplet
**Goal.** Build a sidebar Gramplet that reads the active person from the database and shows their direct events, refreshing whenever the active person changes or the database is updated.
The Hello Gramplet from [02-get-started]] was static text. This one is dynamic — it subscribes to signals and re-reads the DB on each update.
=== Layout ===
Two files in a new folder <code>PersonEvents/</code>:
<pre>PersonEvents/
├── PersonEvents.gpr.py
└── personevents.py</pre>
=== <code>PersonEvents/PersonEvents.gpr.py</code> ===
<syntaxhighlight lang="python">register(
GRAMPLET,
id="PersonEvents",
name=_("Person Events"),
description=_("Lists the active person's direct events."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="personevents.py",
gramplet="PersonEventsGramplet",
gramplet_title=_("Events"),
height=200,
expand=True,
)</syntaxhighlight>
<code>height</code> and <code>expand</code> are Gramplet-specific layout fields; the rest are the same registration shape introduced in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals#the-gprpy-registration-file|05-fundamentals → The `.gpr.py` registration file]].
=== <code>PersonEvents/personevents.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.plug import Gramplet
_ = glocale.get_addon_translator(__file__).gettext
class PersonEventsGramplet(Gramplet):
"""List the active person's direct events; refresh on changes."""
def init(self):
"""Build the static parts of the UI once."""
self.set_use_markup(True)
self.set_text(_("No active person."))
def db_changed(self):
"""Subscribe to DB signals each time the active DB changes."""
self.connect(self.dbstate.db, "person-update", self.update)
self.connect(self.dbstate.db, "person-delete", self.update)
self.connect(self.dbstate.db, "event-update", self.update)
def active_changed(self, handle):
"""Active person changed — re-render."""
self.update()
def main(self):
"""Pull events for the active person and render them."""
person_handle = self.get_active("Person")
if not person_handle:
self.set_text(_("No active person."))
return
person = self.dbstate.db.get_person_from_handle(person_handle)
if person is None:
self.set_text(_("Active person not found."))
return
lines = [f"<b>{person.gramps_id}</b>\n"]
for event_ref in person.get_event_ref_list():
event = self.dbstate.db.get_event_from_handle(event_ref.ref)
if event is None:
continue
date = event.get_date_object()
lines.append(f"{event.get_type()} {date}")
self.set_text("\n".join(lines))</syntaxhighlight>
=== What's new vs. Hello Gramplet ===
* '''<code>db_changed()</code>''' subscribes to DB signals. Using <code>self.connect(...)</code> (defined on <code>Gramplet</code>) instead of <code>self.dbstate.db.connect(...)</code> means Gramps tracks the subscription keys for you and disconnects them automatically when the gramplet closes or the DB swaps out. The forgotten-disconnect bug class is gone.
* '''<code>active_changed(handle)</code>''' is called by Gramps when the user selects a different person in the active view. The default does nothing; calling <code>self.update()</code> triggers a redraw.
* '''<code>get_active("Person")</code>''' returns the handle of the active person for the current view, or <code>None</code>. It honours navigation context — in a Place view it returns the active place, etc.
* '''<code>set_use_markup(True)</code>''' lets <code>set_text()</code> interpret Pango markup (<code><b></code>, <code><i></code>, …); see [[Gramplets_development#Textual_Output_Methods|Gramplet textual methods]].
=== Try it ===
Drop the folder into your user plugin directory (or symlink it if you're on Gramps 6.1+), restart Gramps, open a tree, and add the Gramplet from the sidebar menu. Click around different people — the displayed events should change with the selection.
For the API surface this tutorial used (handles, refs, <code>iter_*</code>, <code>commit_*</code>), see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. For the signal inventory, see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals#signals-addons-reacting-to-changes|05-fundamentals → Signals]].
== A simple Tool ==
'''Goal.''' A menu-launched Tool that scans the database for people with no recorded birth date and shows the list in a dialog.
Tools differ from gramplets in two ways: they're invoked from the Tools menu (not always visible), and they always carry an Options class — even a tool with no options must register an empty <code>ToolOptions</code> subclass.
=== Layout ===
<pre>MissingBirthDates/
├── MissingBirthDates.gpr.py
└── missingbirthdates.py</pre>
=== <code>MissingBirthDates/MissingBirthDates.gpr.py</code> ===
<syntaxhighlight lang="python">register(
TOOL,
id="MissingBirthDates",
name=_("Missing Birth Dates"),
description=_("Lists people with no recorded birth date."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="missingbirthdates.py",
category=TOOL_ANAL,
toolclass="MissingBirthDates",
optionclass="MissingBirthDatesOptions",
tool_modes=[TOOL_MODE_GUI],
)</syntaxhighlight>
<code>category=TOOL_ANAL</code> puts the tool under ''Tools → Analysis and Exploration''. Other categories (<code>TOOL_DBPROC</code>, <code>TOOL_DBFIX</code>, …) are listed in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds#tool|04-addon-kinds → `TOOL`]].
=== <code>MissingBirthDates/missingbirthdates.py</code> ===
<syntaxhighlight lang="python">from gi.repository import Gtk
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gui.dialog import OkDialog
from gramps.gui.plug import tool
_ = glocale.get_addon_translator(__file__).gettext
class MissingBirthDates(tool.Tool):
"""Scan the DB and report people with no recorded birth date."""
def __init__(self, dbstate, user, options_class, name, callback=None):
tool.Tool.__init__(self, dbstate, options_class, name)
db = dbstate.db
missing = []
for person in db.iter_people():
birth_ref = person.get_birth_ref()
if birth_ref is None:
missing.append(person)
continue
event = db.get_event_from_handle(birth_ref.ref)
if event is None or event.get_date_object().is_empty():
missing.append(person)
if not missing:
OkDialog(
_("Missing Birth Dates"),
_("Every person has a recorded birth date."),
parent=user.uistate.window,
)
return
lines = [f"{p.gramps_id}: {p.get_primary_name().get_name()}"
for p in missing]
OkDialog(
_("Missing Birth Dates"),
_("{n} people with no recorded birth date:\n\n{listing}").format(
n=len(missing),
listing="\n".join(lines),
),
parent=user.uistate.window,
)
class MissingBirthDatesOptions(tool.ToolOptions):
"""No options — placeholder required by the tool framework."""</syntaxhighlight>
=== What's new ===
* '''<code>tool.Tool.__init__(self, dbstate, options_class, name)</code>''' — the base-class constructor. The body of <code>__init__</code> is ''where the tool runs''; there's no separate <code>run()</code> method for GUI tools.
* '''<code>MissingBirthDatesOptions</code>''' is required even though we have no options. The <code>register(...)</code> call names it via <code>optionclass</code>, and Gramps would refuse to load the tool without it.
* '''<code>OkDialog</code>''' is the simplest modal report-back surface; for richer output, build a <code>Gtk.Dialog</code> directly (see <code>gramps/plugins/tool/dumpgenderstats.py</code> for the standard recipe).
=== Writing data ===
If your tool ''modifies'' the database, all writes go inside a <code>DbTxn</code>:
<syntaxhighlight lang="python">from gramps.gen.db import DbTxn
with DbTxn(_("Mark unreferenced media private"), db) as trans:
for media in db.iter_media():
if not db.find_backlink_handles(media.handle):
media.set_privacy(True)
db.commit_media(media, trans)</syntaxhighlight>
The transaction message is user-visible in the Undo history; translate it. See [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access#mutating-data|06-data-access → Mutating data]] for the full pattern.
=== Try it ===
After restart, the tool appears in ''Tools → Analysis and Exploration → Missing Birth Dates''. Run it on <code>example.gramps</code> to see the dialog.
== A text Report ==
'''Goal.''' A simple text report that summarises the database — number of people, number of families, count by gender. Produces the same content through PDF, HTML, ODF, or any other docgen-supported format.
Reports are the heaviest of the everyday addon kinds. Three pieces work together:
* A '''Report''' class that knows how to walk the data and emit it as paragraphs and tables, leaving format details to the docgen.
* An '''Options''' class that defines user-adjustable options and the paragraph / font styles.
* A '''registration''' call wiring both into the menu.
=== Layout ===
<pre>DbSummary/
├── DbSummary.gpr.py
└── dbsummary.py</pre>
=== <code>DbSummary/DbSummary.gpr.py</code> ===
<syntaxhighlight lang="python">register(
REPORT,
id="DbSummary",
name=_("Database Summary"),
description=_("Produces a short summary of the family tree."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="dbsummary.py",
category=CATEGORY_TEXT,
require_active=False,
reportclass="DbSummaryReport",
optionclass="DbSummaryOptions",
report_modes=[REPORT_MODE_GUI, REPORT_MODE_CLI],
)</syntaxhighlight>
<code>category=CATEGORY_TEXT</code> makes this a text report — Gramps will offer the user the text-output document backends (PDF, ODF, plain text, …). <code>require_active=False</code> because a database summary doesn't need a specific active person.
=== <code>DbSummary/dbsummary.py</code> ===
<syntaxhighlight lang="python">from collections import Counter
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.lib import Person
from gramps.gen.plug import docgen
from gramps.gen.plug.report import MenuReportOptions, Report
from gramps.gen.plug.report import stdoptions
_ = glocale.get_addon_translator(__file__).gettext
class DbSummaryReport(Report):
"""A text report summarising the database."""
def __init__(self, database, options_class, user):
Report.__init__(self, database, options_class, user)
self.set_locale(
options_class.menu.get_option_by_name("trans").get_value()
)
self._count()
def _count(self):
"""Walk every Person and tally."""
self.total = 0
gender_counts = Counter()
surnames = Counter()
for person in self.database.iter_people():
self.total += 1
gender_counts[person.get_gender()] += 1
primary = person.get_primary_name()
surnames[primary.get_primary_surname().get_surname()] += 1
self.gender_counts = gender_counts
self.unique_surnames = len(surnames)
self.top_surname = (
surnames.most_common(1)[0] if surnames else (_("(none)"), 0)
)
def write_report(self):
"""Emit paragraphs into self.doc."""
self.doc.start_paragraph("DBS-Title")
self.doc.write_text(self._("Database Summary"))
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Total persons: {n}").format(n=self.total))
self.doc.end_paragraph()
for gender_code, label in [
(Person.MALE, _("Males")),
(Person.FEMALE, _("Females")),
(Person.UNKNOWN, _("Unknown gender")),
]:
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("{label}: {n}").format(
label=label,
n=self.gender_counts.get(gender_code, 0),
)
)
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Unique surnames: {n}").format(n=self.unique_surnames)
)
self.doc.end_paragraph()
self.doc.start_paragraph("DBS-Normal")
self.doc.write_text(
self._("Most common surname: {name} ({n})").format(
name=self.top_surname[0], n=self.top_surname[1])
)
self.doc.end_paragraph()
class DbSummaryOptions(MenuReportOptions):
"""Options form and default styles for DbSummaryReport."""
def add_menu_options(self, menu):
category = _("Report Options")
stdoptions.add_localization_option(menu, category)
def make_default_style(self, default_style):
# Title style: 18 pt bold sans-serif, centred, header level 1.
font = docgen.FontStyle()
font.set_size(18)
font.set_type_face(docgen.FONT_SANS_SERIF)
font.set_bold(True)
para = docgen.ParagraphStyle()
para.set_header_level(1)
para.set_alignment(docgen.PARA_ALIGN_CENTER)
para.set_font(font)
para.set_description(_("Style used for the title of the report."))
default_style.add_paragraph_style("DBS-Title", para)
# Body style: 12 pt serif.
font = docgen.FontStyle()
font.set_size(12)
font.set_type_face(docgen.FONT_SERIF)
para = docgen.ParagraphStyle()
para.set_font(font)
para.set_description(_("Style used for normal report text."))
default_style.add_paragraph_style("DBS-Normal", para)</syntaxhighlight>
=== What's new ===
* '''Two classes, one file.''' The <code>register()</code> call points <code>reportclass</code> at the Report and <code>optionclass</code> at the Options.
* '''<code>self.doc</code> is not a file.''' It's the live document — a docgen backend instance. The report writes paragraphs and text into it regardless of output format.
* '''Paragraph style names are prefixed.''' Use <code>DBS-</code> (or any short prefix unique to your report) on every style name. Reports get composed into Book reports, where every style name has to be unique across all contributing reports.
* '''Localisation is explicit.''' <code>stdoptions.add_localization_option</code> adds the standard "report locale" option to the form; the report reads it with <code>self.set_locale(...)</code> and uses <code>self._()</code> for strings that should follow the ''report's'' chosen locale rather than the UI locale. The leading underscore in <code>self._</code> is intentional.
* '''<code>MenuReportOptions</code>''' is the convenient base; for a no-options report, override only <code>add_menu_options</code> (to add the locale option) and <code>make_default_style</code> (to define paragraph styles).
=== Try it ===
After restart, the report appears in ''Reports → Text Reports → Database Summary''. Run it through any text document backend (PDF, ODF, plain text) to see the same content reformatted by each.
For more on the docgen abstraction, see [[Report_Generation|Report Generation]]. For richer reports (tables, multiple paragraph levels, graphical reports using <code>CATEGORY_DRAW</code>), see [[Report_API|Report API]].
== A Quick View ==
'''Goal.''' A right-click action on a person that lists their siblings — brothers and sisters from every family they're a child in.
Quick Views are the shortest path to a usable report. There's no class to subclass and no options form to maintain — just a <code>run()</code> function and the registration. They're written against the '''Simple Access API''' (<code>SimpleAccess</code>, <code>SimpleDoc</code>), which trades some power for very little code.
=== Layout ===
<pre>Siblings/
├── Siblings.gpr.py
└── siblings.py</pre>
=== <code>Siblings/Siblings.gpr.py</code> ===
<syntaxhighlight lang="python">register(
QUICKVIEW,
id="Siblings",
name=_("Siblings"),
description=_("Lists the active person's siblings."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="siblings.py",
category=CATEGORY_QR_PERSON,
runfunc="run",
)</syntaxhighlight>
<code>category=CATEGORY_QR_PERSON</code> puts the entry on the person context menu. <code>runfunc="run"</code> names the function Gramps calls. The full set of categories is listed in [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds#quickview|04-addon-kinds → `QUICKVIEW`]].
=== <code>Siblings/siblings.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.simple import SimpleAccess, SimpleDoc
from gramps.gui.plug.quick import QuickTable
_ = glocale.get_addon_translator(__file__).gettext
def run(database, document, person):
"""Display all siblings of the given person."""
sdb = SimpleAccess(database)
sdoc = SimpleDoc(document)
sdoc.title(_("Siblings of {name}").format(name=sdb.name(person)))
sdoc.paragraph("")
table = QuickTable(sdb)
table.columns(_("Person"), _("Gender"), _("Birth date"))
own_gid = sdb.gid(person)
for family in sdb.child_in(person):
for child in sdb.children(family):
if sdb.gid(child) == own_gid:
continue
table.row(child, sdb.gender(child), sdb.birth_date(child))
document.has_data = True
table.write(sdoc)</syntaxhighlight>
=== What's new ===
* '''<code>run(database, document, person)</code>''' — the function signature is fixed by the QuickView kind. The third argument is the ''selected object'' of the category (<code>CATEGORY_QR_PERSON</code> → person, <code>CATEGORY_QR_FAMILY</code> → family, …).
* '''<code>SimpleAccess</code>''' is the high-level read interface — <code>sdb.children(family)</code>, <code>sdb.birth_date(person)</code>, <code>sdb.name(person)</code>. It hides handle dereferencing, refs, and date formatting. For the full surface, see [[Simple_Access_API|Simple Access API]].
* '''<code>SimpleDoc</code>''' is the matching write interface — <code>sdoc.title(...)</code>, <code>sdoc.paragraph(...)</code>, <code>sdoc.header1(...)</code>.
* '''<code>QuickTable</code>''' builds an interactive table where each row links back to a real Gramps object — clicking a person opens that person.
* '''<code>document.has_data = True</code>''' tells Gramps the report produced output. When all rows are filtered out, the empty-state path triggers instead.
=== Try it ===
After restart, right-click any person in the People view or the person editor. ''Quick View → Siblings'' appears in the menu. The result opens in a Quick View window; clicking a row in the table opens that person.
For Quick Views that don't fit the Simple Access surface, you can reach for the full DB API — see [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]]. The two are complementary; a complex Quick View can use both.
== A custom filter Rule ==
'''Goal.''' A filter rule "Has at least N children" that the user can add to a custom person filter from the Filter Editor.
Filter rules are the smallest addon kind by line count and the one with the most reuse: a single rule, written once, drops into every filter the user composes — search, narrative website, reports, gramplets that accept a filter.
=== Layout ===
<pre>HasNChildren/
├── HasNChildren.gpr.py
└── hasnchildren.py</pre>
=== <code>HasNChildren/HasNChildren.gpr.py</code> ===
<syntaxhighlight lang="python">register(
RULE,
id="HasNChildren",
name=_("People with at least N children"),
description=_("Matches people who have at least N children."),
version="1.0.0",
gramps_target_version="6.0",
status=STABLE,
fname="hasnchildren.py",
ruleclass="HasNChildren",
namespace="Person",
)</syntaxhighlight>
<code>namespace="Person"</code> says this rule applies to people. The other namespaces (<code>Family</code>, <code>Event</code>, <code>Place</code>, <code>Source</code>, <code>Citation</code>, <code>Repository</code>, <code>Media</code>, <code>Note</code>) get their own rules — Gramps' filter editor groups rules by namespace.
=== <code>HasNChildren/hasnchildren.py</code> ===
<syntaxhighlight lang="python">from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.filters.rules import Rule
_ = glocale.get_addon_translator(__file__).gettext
class HasNChildren(Rule):
"""Matches people with at least N children."""
labels = [_("Minimum count:")]
name = _("People with at least N children")
category = _("Family filters")
description = _("Matches people with at least N children")
def apply_to_one(self, db, person):
try:
minimum = int(self.list[0])
except (TypeError, ValueError):
return False
total = 0
for family_handle in person.get_family_handle_list():
family = db.get_family_from_handle(family_handle)
if family is None:
continue
total += len(family.get_child_ref_list())
if total >= minimum:
return True
return False</syntaxhighlight>
=== What's new ===
* '''<code>labels</code>''' declares the user-prompted arguments — one entry per text box in the filter-editor dialog. The user's typed values arrive on <code>self.list</code> in the same order. Always parse defensively; <code>self.list[0]</code> is a string straight from the GUI.
* '''<code>name</code>, <code>category</code>, <code>description</code>''' are class attributes — Gramps reads them off the class (no instance needed) when building the Add Rule dialog. <code>category</code> is the section the rule appears under in that dialog.
* '''<code>apply_to_one(self, db, person)</code>''' is the per-object hook. It returns <code>True</code> for a match, <code>False</code> for a non-match. Gramps calls it for every person in the namespace when applying the filter. On Gramps 6.0 the API is <code>apply_to_one</code>; older releases used <code>apply</code> (see [https://github.com/gramps-project/gramps/blob/maintenance/gramps60/gramps/gen/filters/rules/_rule.py#L162 gramps/gen/filters/rules/_rule.py:162]).
=== Optional hooks ===
* '''<code>prepare(self, db, user)</code>''' — called once before the rule is applied to many objects, on demand. Use it to precompute lookup tables when <code>apply_to_one</code> would otherwise repeat expensive work. Pair with <code>reset()</code> to release memory afterwards.
* '''<code>allow_regex = True</code>''' — opt the first label into regex input.
=== Try it ===
After restart, ''Edit → Person Filter Editor → Add → Add Rule'' shows "People with at least N children" under ''Family filters''. The user types a number in the "Minimum count" field; the rule does the rest.
The rule is also visible from gramplets like ''Filter Gramplet'' and as an input to any tool or report that accepts a person filter — no extra work needed; rules are uniform across the framework.
== See also ==
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Getting_Started|02-get-started]] — the prerequisites and the development loop these tutorials build on.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Addon_Kinds|04-addon-kinds]] — registration details per kind.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Fundamentals|05-fundamentals]] — <code>.gpr.py</code> fields, signals, <code>requires_mod</code>, lifecycle hooks.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Data_access|06-data-access]] — the DB API patterns used by these tutorials.
* [[User:Eduralph/Sandbox/Gramps_6.0_Wiki_Manual_-_Addon_Development_-_Testing|08-testing]] — how to test what you just wrote without launching Gramps.
* [[Report_API|Report API]], [[Report_Generation|Report Generation]] — depth on the docgen abstraction.
* [[Simple_Access_API|Simple Access API]] — the Quick View read surface.
{{stub}}