Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions BirthdaysGramplet/BirthdaysGramplet.gpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#
# Copyright (C) 2010 Peter Potrowl <peter017@gmail.com>
# Copyright (C) 2019 Matthias Kemmer <matt.familienforschung@gmail.com>
# Copyright (C) 2026 Javad Razavian <javadr@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -25,11 +26,13 @@
name=_("Birthdays"),
description=_("a gramplet that displays the birthdays of the living people"),
status=STABLE,
version = '1.1.16',
version = '1.1.21',
fname="BirthdaysGramplet.py",
authors = ["Peter Potrowl", "Matthias Kemmer", "Javad Razavian" ],
authors_email = ["peter017@gmail.com", "matt.familienforschung@gmail.com", "javadr@gmail.com" ],
height=200,
gramplet="BirthdaysGramplet",
gramps_target_version="6.0",
gramplet_title=_("Birthdays"),
help_url="BirthdaysGramplet",
)
help_url="Addon:BirthdaysGramplet",
)
38 changes: 28 additions & 10 deletions BirthdaysGramplet/BirthdaysGramplet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#
# Copyright (C) 2010 Peter Potrowl <peter017@gmail.com>
# Copyright (C) 2019 Matthias Kemmer <matt.familienforschung@gmail.com>
# Copyright (C) 2026 Javad Razavian <javadr@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -22,7 +23,7 @@
from gramps.gen.plug import Gramplet
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.display.name import displayer as name_displayer
from gramps.gen.lib.date import Today, Date
from gramps.gen.lib.date import Today, Date, gregorian
import gramps.gen.datehandler
from gramps.gen.config import config
from gramps.gen.plug.menu import EnumeratedListOption
Expand All @@ -37,11 +38,18 @@ class BirthdaysGramplet(Gramplet):
def init(self):
self.set_text(_("No Family Tree loaded."))
self.max_age = config.get('behavior.max-age-prob-alive')
self.sort_mode = 'proximity'

def build_options(self):
"""Build the configuration options"""
db = self.dbstate.db

name_sort = _("Sort birthdays by")
self.opt_sort = EnumeratedListOption(name_sort, self.sort_mode)
self.opt_sort.add_item("proximity", _("Proximity to current date"))
self.opt_sort.add_item("month_day", _("Month and day"))


name_ignore = _("Ignore birthdays with tag")
name_only = _("Only show birthdays with tag")
self.opt_ignore = EnumeratedListOption(name_ignore, self.ignore_tag)
Expand All @@ -56,26 +64,30 @@ def build_options(self):
self.opt_ignore.add_item(tag_name, tag_name)
self.opt_only.add_item(tag_name, tag_name)

self.add_option(self.opt_sort)
self.add_option(self.opt_ignore)
self.add_option(self.opt_only)

def save_options(self):
"""Save gramplet configuration data"""
self.sort_mode = self.opt_sort.get_value()
self.ignore_tag = self.opt_ignore.get_value()
self.only_tag = self.opt_only.get_value()

def save_update_options(self, obj):
"""Save a gramplet's options to file"""
self.save_options()
self.gui.data = [self.ignore_tag, self.only_tag]
self.gui.data = [self.sort_mode, self.ignore_tag, self.only_tag]
self.update()

def on_load(self):
"""Load stored configuration data"""
if len(self.gui.data) == 2:
self.ignore_tag = self.gui.data[0]
self.only_tag = self.gui.data[1]
if len(self.gui.data) == 3:
self.sort_mode = self.gui.data[0]
self.ignore_tag = self.gui.data[1]
self.only_tag = self.gui.data[2]
else:
Comment on lines +85 to 89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing Birthday tag filters on load

When a user already has this gramplet saved from the previous version, self.gui.data still contains the two values that used to be written ([ignore_tag, only_tag]). This branch treats any length other than 3 as a fresh configuration and clears both tag filters, so upgrading silently loses saved Birthday filter settings until the user reselects them; please handle len(self.gui.data) == 2 as legacy data and default only the new sort option.

Useful? React with 👍 / 👎.

self.sort_mode = 'proximity'
self.ignore_tag = ''
self.only_tag = ''

Expand Down Expand Up @@ -114,8 +126,13 @@ def main(self):
# calculate age and days until birthday
self.__calculate(database, person)

# Reverse sort on number of days from now:
self.result.sort(key=lambda item: -item[0])
# Sort based on user preference:
sort_by = self.opt_sort.get_value()
if sort_by == "proximity":
self.result.sort(key=lambda item: -item[0])
else:
self.result.sort(key=lambda item: (item[2].get_month(),
item[2].get_day()))
self.clear_text()

# handle text shown in gramplet
Expand All @@ -137,9 +154,10 @@ def __calculate(self, database, person):
birth = database.get_event_from_handle(birth_ref.ref)
birth_date = birth.get_date_object()
if birth_date.is_regular():
birth_greg = gregorian(birth_date)
birthday_this_year = Date(today.get_year(),
birth_date.get_month(),
birth_date.get_day())
birth_greg.get_month(),
birth_greg.get_day())
next_age = birthday_this_year - birth_date
# (0 year, months, days) between now and birthday of this
# year (positive if passed):
Expand All @@ -153,4 +171,4 @@ def __calculate(self, database, person):
else: # passed; add it for next year's birthday
self.result.append((diff_days - 365,
next_age[0] + 1,
birth_date, person))
birth_date, person))
4 changes: 2 additions & 2 deletions Form/CensusCheckQuickview.gpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
id = 'censuscheckquickview',
name = _("CensusCheck"),
description= _("Check whether any Census events are missing for a person and some of their descendents"),
version = '1.0.5',
version = '1.0.6',
gramps_target_version = '6.0',
status = STABLE,
fname = 'CensusCheckQuickview.py',
Expand All @@ -22,7 +22,7 @@
id = 'censuscheckupquickview',
name = _("CensusCheckUp"),
description= _("Check whether any Census events are missing for a person and some of their ancestors"),
version = '1.0.5',
version = '1.0.6',
gramps_target_version = '6.0',
status = STABLE,
fname = 'CensusCheckUpQuickview.py',
Expand Down
4 changes: 0 additions & 4 deletions Form/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
#
# ---------------------------------------------------------------
from form_validator import (
get_form_warnings,
validate_form_dom,
validate_form_element,
)
Expand Down Expand Up @@ -200,9 +199,6 @@ def __load_file(self, full_path):
"\n".join(errors),
)

for warning in get_form_warnings(dom):
LOG.warning("In %s: %s", full_path, warning)

try:
self.__load_definitions(dom)
finally:
Expand Down
65 changes: 0 additions & 65 deletions Form/form_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,71 +153,6 @@ def validate_form_dom(dom: xml.dom.minidom.Document) -> list[str]:
return errors


def get_form_warnings(dom: xml.dom.minidom.Document) -> list[str]:
"""
Collect non-fatal warnings about a parsed form definitions DOM.
Warnings describe likely authoring mistakes that do not prevent the
form from loading. Currently covers Gramps bug 11010's observation
that a section's ``<column>`` ``<size>`` values are expected to sum
to 100 — sections that declare explicit sizes on every column but do
not sum to 100 are reported as warnings so callers can log them
without blocking the form from loading.
Sections without any sized columns, or with only some columns
sized, are skipped because the intent is ambiguous.
:param dom: a parsed ``xml.dom.minidom.Document``
:returns: a list of human-readable warning messages; empty when
nothing questionable is detected
"""
warnings: list[str] = []
top = dom.getElementsByTagName("forms")
if not top:
return warnings

for form in top[0].getElementsByTagName("form"):
form_id = (
form.attributes["id"].value
if "id" in form.attributes
else "<missing id>"
)
for section in form.getElementsByTagName("section"):
role = (
section.attributes["role"].value
if "role" in section.attributes
else "<missing role>"
)
columns = section.getElementsByTagName("column")
if not columns:
continue

sizes: list[int] = []
all_sized = True
for column in columns:
size_nodes = column.getElementsByTagName("size")
if not size_nodes or not size_nodes[0].childNodes:
all_sized = False
break
try:
sizes.append(int(size_nodes[0].childNodes[0].data))
except ValueError:
all_sized = False
break
if not all_sized:
continue

total = sum(sizes)
if total != 100:
warnings.append(
"Form '%s': section '%s' column sizes sum to %d "
"(expected 100); form will still load but column "
"widths may not render as intended"
% (form_id, role, total)
)
return warnings


def parse_and_validate(path: str) -> tuple[xml.dom.minidom.Document | None, list[str]]:
"""
Parse ``path`` as XML and validate it against the form schema.
Expand Down
2 changes: 1 addition & 1 deletion Form/formgramplet.gpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
name=_("Form Gramplet"),
description=_("Gramplet interface for Forms"),
status=STABLE,
version = '2.0.55',
version = '2.0.56',
gramps_target_version="6.0",
navtypes=["Person"],
fname="formgramplet.py",
Expand Down
121 changes: 0 additions & 121 deletions Form/tests/test_form_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
# ------------------------
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from form_validator import (
get_form_warnings,
parse_and_validate,
split_family_title,
validate_form_dom,
Expand Down Expand Up @@ -394,125 +393,5 @@ def test_all_builtin_form_files_validate(self):
)


# ---------------------------------------------------------------------------
# get_form_warnings — non-fatal authoring warnings (Gramps bug 11010)
# ---------------------------------------------------------------------------
class TestGetFormWarnings(unittest.TestCase):
"""
Column sizes are expected to sum to 100. Rather than reject the
form, ``get_form_warnings`` flags suspect sections so the caller can
log them — 78 shipped definition files currently violate this rule
without breaking rendering, so escalating to an error would be
user-hostile.
"""

def test_section_without_columns_has_no_warning(self):
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='Primary' type='person'/>
</form>
</forms>
"""))
self.assertEqual(get_form_warnings(dom), [])

def test_columns_without_any_size_have_no_warning(self):
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='Primary' type='person'>
<column><_attribute>Name</_attribute></column>
<column><_attribute>Age</_attribute></column>
</section>
</form>
</forms>
"""))
self.assertEqual(get_form_warnings(dom), [])

def test_columns_summing_to_100_have_no_warning(self):
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='Primary' type='person'>
<column><_attribute>A</_attribute><size>60</size></column>
<column><_attribute>B</_attribute><size>40</size></column>
</section>
</form>
</forms>
"""))
self.assertEqual(get_form_warnings(dom), [])

def test_columns_not_summing_to_100_emit_warning(self):
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='Primary' type='person'>
<column><_attribute>A</_attribute><size>25</size></column>
</section>
</form>
</forms>
"""))
warnings = get_form_warnings(dom)
self.assertEqual(len(warnings), 1)
self.assertIn("sum to 25", warnings[0])
self.assertIn("F1", warnings[0])
self.assertIn("Primary", warnings[0])

def test_partially_sized_columns_have_no_warning(self):
"""
When only some columns declare a ``<size>``, the author's intent
is ambiguous (mixed relative/absolute sizing) so the check is
skipped to avoid false positives.
"""
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='Primary' type='person'>
<column><_attribute>A</_attribute><size>25</size></column>
<column><_attribute>B</_attribute></column>
</section>
</form>
</forms>
"""))
self.assertEqual(get_form_warnings(dom), [])

def test_warnings_are_independent_of_errors(self):
"""
Warnings are structurally orthogonal to errors: a malformed form
still produces warnings for its well-formed sibling.
"""
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='BAD' type='Marriage' title='x'>
<section role='Family' type='family' title='NoSlash'/>
</form>
<form id='GOOD' type='Census' title='x'>
<section role='Primary' type='person'>
<column><_attribute>A</_attribute><size>30</size></column>
</section>
</form>
</forms>
"""))
warnings = get_form_warnings(dom)
self.assertEqual(len(warnings), 1)
self.assertIn("GOOD", warnings[0])

def test_multiple_misaligned_sections_all_reported(self):
dom = _dom_from_string(textwrap.dedent("""\
<forms>
<form id='F1' type='Census' title='x'>
<section role='A' type='person'>
<column><_attribute>X</_attribute><size>40</size></column>
</section>
<section role='B' type='person'>
<column><_attribute>Y</_attribute><size>70</size></column>
</section>
</form>
</forms>
"""))
warnings = get_form_warnings(dom)
self.assertEqual(len(warnings), 2)


if __name__ == "__main__":
unittest.main()
Loading