Skip to content

Commit dfd437d

Browse files
authored
Merge pull request #19 from eduralph/sync/upstream-maintenance-gramps61-auto
Sync addons-source@maintenance/gramps61 with upstream (2026-05-23)
2 parents fe9a87d + 07dbfb2 commit dfd437d

83 files changed

Lines changed: 3992 additions & 1152 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CombinedView/combinedview.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
id="combinedview",
2424
name=_("Combined"),
2525
description=_("A view showing relationships and events for a person"),
26-
version = '2.0.17',
26+
version = '2.0.18',
2727
gramps_target_version="6.1",
2828
status=STABLE,
2929
fname="combinedview.py",

CombinedView/combinedview.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,8 @@ def filter_editor(self, *obj):
474474
return
475475

476476
def edit_active(self, *obj):
477-
self.active_page.edit_active()
477+
if self.active_page is not None:
478+
self.active_page.edit_active()
478479

479480
def change_db(self, db):
480481
#reset the connects
@@ -498,9 +499,18 @@ def object_changed(self, obj_type, handle):
498499
def change_object(self, obj_tuple):
499500

500501
if obj_tuple is None:
502+
# No active object, e.g. the family tree was closed. Clear the
503+
# display instead of leaving people from the closed tree on
504+
# screen: their handles no longer resolve, so editing one would
505+
# raise HandleError (bug #12572, bug #14226).
506+
for page in self.pages.values():
507+
page.disable_actions(self.uimanager)
508+
self.uimanager.update_menu()
509+
list(map(self.header.remove, self.header.get_children()))
510+
list(map(self.stack.remove, self.stack.get_children()))
511+
self.active_page = None
501512
return
502513

503-
504514
if self.redrawing:
505515
return False
506516
self.redrawing = True

DescendantBooks/DescendantBooks.gpr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
id="DescendantBook",
2222
name=_("Descendant Book"),
2323
description=_("Produces one or more descendant reports based on a supplied query."),
24-
version = '1.1.36',
24+
version = '1.1.37',
2525
gramps_target_version="6.1",
2626
status=STABLE,
2727
fname="DescendantBookReport.py",
@@ -42,7 +42,7 @@
4242
description=_(
4343
"Produces one or more detailed descendant reports based on a supplied query."
4444
),
45-
version = '1.1.36',
45+
version = '1.1.37',
4646
gramps_target_version="6.1",
4747
status=STABLE,
4848
fname="DetailedDescendantBookReport.py",

DescendantBooks/DetailedDescendantBookReport.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,35 @@ def write_report_ref(self, person, main_person):
788788

789789
def append_event(self, event_ref, family = False):
790790

791-
(repno, gen, per, mate, name) = self.report_app_ref[self.phandle][0] # get first reference to the person
791+
# The pre-pass under `if self.dubperson:` in write_report() is the
792+
# only path that fills report_app_ref. When "Omit duplicate
793+
# ancestors" is unselected but an index option (Index of Dates /
794+
# Places / Names) is enabled, the indexes still call append_event
795+
# for every event — and a missing handle here raised KeyError
796+
# (bug 14051) or AttributeError (bug 12857, before the partial
797+
# fix). Populate the entry on first encounter so subsequent
798+
# encounters (which happen when omit-duplicates is off and the
799+
# same person appears in more than one per-ascendant report)
800+
# keep using the first encounter's coordinates — matching the
801+
# `[0]` semantic the omit-duplicates path emits, so an index
802+
# entry resolves to the same canonical document position
803+
# regardless of the omit-duplicates setting.
804+
if (self.phandle not in self.report_app_ref
805+
or not self.report_app_ref[self.phandle]):
806+
if self.phandle in self.dnumber:
807+
per = self.dnumber[self.phandle]
808+
elif self.phandle in self.dmates \
809+
and self.dmates[self.phandle] in self.dnumber:
810+
per = self.dnumber[self.dmates[self.phandle]]
811+
else:
812+
per = "?"
813+
person = self.database.get_person_from_handle(self.phandle)
814+
name = person.get_primary_name().get_name()
815+
self.report_app_ref[self.phandle] = [
816+
(self.report_count, self.generation + 1, per, False, name)
817+
]
818+
(repno, gen, per, mate, name) = \
819+
self.report_app_ref[self.phandle][0]
792820

793821
text = ""
794822
event = self.database.get_event_from_handle(event_ref.ref)

DescendantBooks/tests/__init__.py

Whitespace-only changes.
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#
2+
# Gramps - a GTK+/GNOME based genealogy program
3+
#
4+
# Copyright (C) 2026 Gramps Development Team
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19+
#
20+
21+
"""
22+
Regression test for Mantis bug 14051.
23+
24+
`DetailedDescendantBookReport.append_event` reads
25+
``self.report_app_ref[self.phandle][0]`` to build the "Ref: ..." text
26+
that prefixes each index-of-dates / index-of-places entry. The lookup
27+
table ``report_app_ref`` is only filled by the up-front reference pass
28+
guarded by ``if self.dubperson:`` (the "omit duplicate ancestors"
29+
option). When that option is unselected but Index of Dates or Index of
30+
Places is enabled, ``append_event`` still runs from
31+
``write_person_info`` / ``__write_family_events`` and raises against
32+
the empty / missing table — historically an ``AttributeError`` (before
33+
the partial fix for bugs 12857/12859 unconditionally initialised the
34+
dict), now a ``KeyError`` on the current handle.
35+
36+
This test exercises ``append_event`` in isolation with the settings
37+
combination from bug 14051 (``dubperson=False`` + indexes enabled) and
38+
asserts it completes without raising and that the indexes receive an
39+
entry referencing the current writing-pass context.
40+
"""
41+
42+
import os
43+
import sys
44+
import types
45+
import unittest
46+
from unittest import mock
47+
48+
# Make the addon importable from its parent directory, matching the
49+
# JSON / TMGimporter / Form test convention. Required when this test
50+
# is loaded by its dotted path (`DescendantBooks.tests.<module>`).
51+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
52+
53+
# Stub RunReport so importing DetailedDescendantBookReport does not pull
54+
# in gramps.gui (which requires gi.require_version('Gtk', '4.0') and a
55+
# display). The append_event method does not touch the report dialog.
56+
_run_report_stub = types.ModuleType("RunReport")
57+
_run_report_stub.RunReport = lambda *a, **kw: None
58+
sys.modules.setdefault("RunReport", _run_report_stub)
59+
60+
import DetailedDescendantBookReport as ddbr
61+
62+
63+
def _make_event(year, place_handle=""):
64+
"""Build a stub event_ref / event pair usable by append_event."""
65+
date_obj = mock.MagicMock()
66+
date_obj.get_year.return_value = year
67+
68+
event = mock.MagicMock()
69+
event.get_date_object.return_value = date_obj
70+
event.get_place_handle.return_value = place_handle
71+
event.get_type.return_value = "Birth"
72+
73+
event_ref = mock.MagicMock()
74+
event_ref.ref = "EVENT-HANDLE"
75+
return event_ref, event, date_obj
76+
77+
78+
class TestAppendEventWithoutOmitDuplicates(unittest.TestCase):
79+
"""append_event must not crash when the reference pre-pass is skipped.
80+
81+
Bug 14051: with ``Omit duplicate ancestors`` off and an index option
82+
on, the pre-pass under ``if self.dubperson:`` never populates
83+
``report_app_ref``, but ``append_event`` is still invoked for every
84+
event because the indexes are enabled.
85+
"""
86+
87+
def _make_report(self):
88+
# Build a DetailedDescendantBookReport without calling __init__
89+
# (the real __init__ takes options/user/database and constructs
90+
# a Bibliography etc., none of which append_event uses).
91+
report = ddbr.DetailedDescendantBookReport.__new__(
92+
ddbr.DetailedDescendantBookReport
93+
)
94+
95+
# Minimum attributes append_event reads. Mirrors what
96+
# write_report would set up before invoking the writing pass.
97+
report.report_app_ref = {} # the bug's empty table
98+
report.index_of_dates = {}
99+
report.index_of_places = {}
100+
report.dnumber = {"P1": "1"}
101+
report.dmates = {}
102+
report.report_count = 3 # third ascendant tree
103+
report.generation = 1 # second generation in it
104+
report.phandle = "P1"
105+
report.inc_index_of_dates = True
106+
report.inc_index_of_places = True
107+
108+
# _ is the report's translation callable; sgettext-style — just
109+
# return the string verbatim so we can assert on it.
110+
report._ = lambda s: s
111+
# _get_date / _get_type return strings; stub to identity-like.
112+
report._get_date = lambda d: "1850"
113+
report._get_type = lambda t: str(t)
114+
115+
# Stub the database so place lookups return a known title.
116+
place = mock.MagicMock()
117+
place.get_title.return_value = "Dublin"
118+
db = mock.MagicMock()
119+
db.get_event_from_handle.return_value = None # set per-test
120+
db.get_place_from_handle.return_value = place
121+
db.get_person_from_handle.return_value = mock.MagicMock(
122+
get_primary_name=lambda: mock.MagicMock(get_name=lambda: "John Doe")
123+
)
124+
report.database = db
125+
return report
126+
127+
def test_append_event_does_not_crash_without_prepopulated_table(self):
128+
"""append_event with an empty report_app_ref must not raise.
129+
130+
Failure pre-fix: raises ``KeyError: 'P1'`` (or, on addon versions
131+
prior to the partial fix for 12857/12859, ``AttributeError`` on
132+
``report_app_ref``) — the exact crash signature from bug 14051.
133+
"""
134+
report = self._make_report()
135+
event_ref, event, _ = _make_event(year=1850, place_handle="PLACE-HANDLE")
136+
report.database.get_event_from_handle.return_value = event
137+
138+
# This is the call the report system makes from
139+
# write_person_info when (inc_index_of_dates or inc_index_of_places)
140+
# is enabled. It must not raise.
141+
report.append_event(event_ref)
142+
143+
# The indexes should have been populated with an entry tagged
144+
# with the writing-pass coordinates at the FIRST encounter of
145+
# this handle — (report_count=3, generation+1=2,
146+
# dnumber[phandle]=1) — matching the `[0]` semantic the
147+
# omit-duplicates path emits (see
148+
# TestRefSemanticsParityWithOmitDuplicates below).
149+
self.assertIn(1850, report.index_of_dates,
150+
"Year 1850 should be indexed")
151+
self.assertIn("Dublin", report.index_of_places,
152+
"Place 'Dublin' should be indexed")
153+
154+
date_entries = list(report.index_of_dates[1850].values())
155+
self.assertTrue(any("Ref: 3 2 1" in entry for entry in date_entries),
156+
"Index entry should reference first-encounter "
157+
"coordinates (3 2 1); got %r" % date_entries)
158+
159+
def test_append_event_for_mate_without_dnumber(self):
160+
"""A mate's event where the mate is not in dnumber must not crash.
161+
162+
``__write_mate`` sets ``self.phandle = mate_handle`` when the
163+
mate is being printed in full; the mate may not have a
164+
``dnumber`` entry (only descendants do). append_event must
165+
still produce an entry with a sensible fallback Ref rather
166+
than KeyError on ``self.dnumber[mate_handle]``.
167+
"""
168+
report = self._make_report()
169+
report.phandle = "MATE-NOT-IN-DNUMBER"
170+
# dmates is empty in __write_mate's Branch A (the not-inc_materef
171+
# path that sets phandle = mate_handle). Leave it that way.
172+
event_ref, event, _ = _make_event(year=1860)
173+
report.database.get_event_from_handle.return_value = event
174+
175+
report.append_event(event_ref)
176+
177+
# The index should still receive an entry; the Ref's "per" field
178+
# falls back gracefully when the mate's dnumber isn't known.
179+
self.assertIn(1860, report.index_of_dates)
180+
181+
182+
class TestRefSemanticsParityWithOmitDuplicates(unittest.TestCase):
183+
"""When the same person/event is processed in multiple per-ascendant
184+
reports (the dubperson=False case with multi-ascendant trees), the
185+
index entry for that event must resolve to the SAME (repno, gen,
186+
per) coordinates that the omit-duplicates path would emit — i.e.
187+
the FIRST encounter's coordinates. Otherwise the dubperson-on and
188+
dubperson-off documents' index entries point to different sections
189+
of their respective documents for the same event.
190+
191+
This test simulates two encounters of person P in reports 1 and 2
192+
against append_event, and compares the resulting index Ref to what
193+
the dubperson-on path's `[0]` read of report_app_ref would have
194+
produced (the first encounter).
195+
"""
196+
197+
def _make_report(self):
198+
report = ddbr.DetailedDescendantBookReport.__new__(
199+
ddbr.DetailedDescendantBookReport
200+
)
201+
report.report_app_ref = {}
202+
report.index_of_dates = {}
203+
report.index_of_places = {}
204+
report.dnumber = {}
205+
report.dmates = {}
206+
report.inc_index_of_dates = True
207+
report.inc_index_of_places = True
208+
report._ = lambda s: s
209+
report._get_date = lambda d: "1850-01-15"
210+
report._get_type = lambda t: str(t)
211+
place = mock.MagicMock()
212+
place.get_title.return_value = "Dublin"
213+
db = mock.MagicMock()
214+
db.get_place_from_handle.return_value = place
215+
db.get_person_from_handle.return_value = mock.MagicMock(
216+
get_primary_name=lambda: mock.MagicMock(get_name=lambda: "John Doe")
217+
)
218+
report.database = db
219+
return report
220+
221+
def _make_event(self, year=1850, place_handle="PLACE-DUBLIN"):
222+
date_obj = mock.MagicMock()
223+
date_obj.get_year.return_value = year
224+
event = mock.MagicMock()
225+
event.get_date_object.return_value = date_obj
226+
event.get_place_handle.return_value = place_handle
227+
event.get_type.return_value = "Birth"
228+
event_ref = mock.MagicMock()
229+
event_ref.ref = "EVENT-HANDLE"
230+
return event_ref, event
231+
232+
def test_dubperson_on_baseline_ref_is_first_encounter(self):
233+
"""Baseline: with the pre-pass populated as it is when
234+
omit-duplicates is on, append_event reads `[0]` — the first
235+
encounter — and writes that Ref into the index."""
236+
report = self._make_report()
237+
# Simulate the dubperson-on pre-pass having populated
238+
# report_app_ref for person P with the first encounter
239+
# (report=1, generation+1=2, dnumber["P"]="3").
240+
report.report_app_ref["P"] = [(1, 2, "3", False, "John Doe")]
241+
report.report_count = 1
242+
report.generation = 1
243+
report.dnumber = {"P": "3"}
244+
report.phandle = "P"
245+
event_ref, event = self._make_event()
246+
report.database.get_event_from_handle.return_value = event
247+
report.append_event(event_ref)
248+
249+
# The index Ref should reference the first encounter (1, 2, 3).
250+
entries = list(report.index_of_places["Dublin"].values())
251+
self.assertTrue(any("Ref: 1 2 3" in e for e in entries),
252+
"dubperson-on baseline must yield Ref '1 2 3'; "
253+
"got %r" % entries)
254+
255+
def test_dubperson_off_multi_encounter_matches_first_encounter(self):
256+
"""The fallback path (omit-duplicates OFF; report_app_ref
257+
un-pre-populated) must produce the SAME Ref as the
258+
omit-duplicates-on baseline above — i.e. the FIRST encounter's
259+
coordinates — even when the same person/event is processed
260+
again in a later report."""
261+
report = self._make_report()
262+
event_ref, event = self._make_event()
263+
report.database.get_event_from_handle.return_value = event
264+
265+
# First encounter in report 1 (P is generation 1 → gen+1=2,
266+
# dnumber 3 within report 1).
267+
report.report_count = 1
268+
report.generation = 1
269+
report.dnumber = {"P": "3"}
270+
report.phandle = "P"
271+
report.append_event(event_ref)
272+
273+
# Second encounter in report 2 (DIFFERENT coordinates: P is
274+
# generation 3 → gen+1=4, dnumber 5 within report 2).
275+
report.report_count = 2
276+
report.generation = 3
277+
report.dnumber = {"P": "5"}
278+
report.phandle = "P"
279+
report.append_event(event_ref)
280+
281+
# The index entry overwrites on each call. The surviving Ref
282+
# must point at the FIRST encounter (1, 2, 3) — matching the
283+
# dubperson-on baseline — NOT the second (2, 4, 5).
284+
entries = list(report.index_of_places["Dublin"].values())
285+
self.assertTrue(any("Ref: 1 2 3" in e for e in entries),
286+
"Multi-encounter index Ref must match the "
287+
"first-encounter coordinates the dubperson-on "
288+
"path would emit; got %r" % entries)
289+
self.assertFalse(any("Ref: 2 4 5" in e for e in entries),
290+
"Multi-encounter index Ref must NOT have "
291+
"drifted to the last encounter's coordinates; "
292+
"got %r" % entries)
293+
294+
295+
if __name__ == "__main__":
296+
unittest.main()

FRWebConnectPack/FRWebPack.gpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
name=_("FR Web Connect Pack"),
1212
description=_("Collection of Web sites for the FR (requires libwebconnect)"),
1313
status=STABLE,
14-
version = '1.0.40',
14+
version = '1.0.41',
1515
gramps_target_version="6.1",
1616
fname="FRWebPack.py",
1717
load_on_reg=True,

0 commit comments

Comments
 (0)