Improve code quality across the codebase

- Add comprehensive type hints to MyTimeline.py methods
  - Add type hints to __init__, change_db, and tooltip formatting methods
  - Use Any for Gramps-specific types that aren't easily importable

- Refactor generate_demo_family.py to use ElementTree
  - Replace string concatenation with xml.etree.ElementTree for proper XML generation
  - Add compatibility handling for Python < 3.9 (ET.indent)
  - Add EventData, PersonData, and FamilyData dataclasses for better structure
  - Add comprehensive type hints to all functions

- Extract magic numbers to named constants
  - Add constants for UI dimensions, timeline heights, dialog sizes
  - Add constants for date calculations and genealogical year ranges
  - Improve code readability and maintainability

- Refactor duplicated code in filter dialog handlers
  - Extract common checkbox handler logic into reusable methods
  - Create _make_group_toggle_handler and _make_child_toggle_handler
  - Eliminate code duplication between event type and family filters

- Improve shell scripts with better error handling
  - Add validation for Gramps installation
  - Improve error messages with actionable troubleshooting steps
  - Use set -euo pipefail for better error detection
  - Add better user guidance in error scenarios
This commit is contained in:
Daniel Viegas 2025-11-29 22:49:16 +01:00
parent 27de315514
commit 5860b3d25c
4 changed files with 557 additions and 285 deletions

View File

@ -32,7 +32,7 @@ import colorsys
import logging
import math
from dataclasses import dataclass
from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING
from typing import Optional, List, Tuple, Any, Set, Dict, TYPE_CHECKING, Union
if TYPE_CHECKING:
from gramps.gen.lib import Event, Person, Family
@ -90,6 +90,16 @@ TOOLTIP_DELAY = 500 # milliseconds
TOOLTIP_MAX_WIDTH = 500
LABEL_BACKGROUND_PADDING = 8
LABEL_BACKGROUND_RADIUS = 5
DEFAULT_TIMELINE_HEIGHT = 1000 # Default height, will be recalculated
DEFAULT_TIMELINE_WIDTH = 800
DEFAULT_DRAWING_AREA_HEIGHT = 600
DEFAULT_DRAWING_AREA_WIDTH = 800
EMPTY_TIMELINE_HEIGHT = 600 # Height when no events
FILTER_DIALOG_WIDTH = 600
FILTER_DIALOG_HEIGHT = 700
MIN_GENEALOGICAL_YEAR = 1000 # Minimum year for genealogical data
DATE_SORT_YEAR_MULTIPLIER = 10000 # Year component in date sort value
DATE_SORT_MONTH_MULTIPLIER = 100 # Month component in date sort value
# Font Constants
FONT_FAMILY = "Sans"
@ -360,7 +370,7 @@ class MyTimelineView(NavigationView):
and filtering of events by type, category, person, and date range.
"""
def __init__(self, pdata, dbstate, uistate, nav_group=0):
def __init__(self, pdata: Any, dbstate: Any, uistate: Any, nav_group: int = 0) -> None:
"""
Initialize the MyTimeline view.
@ -384,7 +394,7 @@ class MyTimelineView(NavigationView):
# UI components
self.scrolledwindow = None
self.drawing_area = None
self.timeline_height = 1000 # Default height, will be recalculated
self.timeline_height = DEFAULT_TIMELINE_HEIGHT
self.zoom_level = 1.0 # Zoom level (1.0 = 100%)
self.min_zoom = 0.5
self.max_zoom = 3.0
@ -498,7 +508,7 @@ class MyTimelineView(NavigationView):
if self.active_event_handle in handle_list:
self._scroll_to_event(self.active_event_handle)
def change_db(self, db) -> None:
def change_db(self, db: Any) -> None:
"""
Called when the database changes.
@ -665,6 +675,57 @@ class MyTimelineView(NavigationView):
else: # 'some'
group_checkbox.set_inconsistent(True)
def _make_group_toggle_handler(self, child_checkboxes: List[Gtk.CheckButton],
updating_flag: List[bool]) -> Any:
"""
Create a handler for group checkbox toggle that toggles all children.
Args:
child_checkboxes: List of child checkboxes to toggle.
updating_flag: List with single boolean to prevent recursion.
Returns:
Callable handler function.
"""
def handler(widget: Gtk.Widget) -> None:
if updating_flag[0]:
return
# Handle inconsistent state - make it consistent
if widget.get_inconsistent():
widget.set_inconsistent(False)
widget.set_active(True)
updating_flag[0] = True
# Toggle all children
is_active = widget.get_active()
for child_cb in child_checkboxes:
child_cb.set_active(is_active)
updating_flag[0] = False
return handler
def _make_child_toggle_handler(self, group_checkbox: Gtk.CheckButton,
child_checkboxes: List[Gtk.CheckButton],
updating_flag: List[bool]) -> Any:
"""
Create a handler for child checkbox toggle that updates group state.
Args:
group_checkbox: The parent group checkbox to update.
child_checkboxes: List of all child checkboxes.
updating_flag: List with single boolean to prevent recursion.
Returns:
Callable handler function.
"""
def handler(widget: Gtk.Widget) -> None:
if updating_flag[0]:
return
self._update_group_checkbox_state(group_checkbox, child_checkboxes)
return handler
def _build_filter_dialog(self) -> Gtk.Dialog:
"""
Build the filter dialog with all filter controls.
@ -673,7 +734,7 @@ class MyTimelineView(NavigationView):
Gtk.Dialog: The filter dialog widget.
"""
dialog = Gtk.Dialog(title=_("Filter Events"), parent=self.uistate.window)
dialog.set_default_size(600, 700)
dialog.set_default_size(FILTER_DIALOG_WIDTH, FILTER_DIALOG_HEIGHT)
dialog.add_button(_("Clear All"), Gtk.ResponseType.REJECT)
dialog.add_button(_("Close"), Gtk.ResponseType.CLOSE)
dialog.add_button(_("Apply"), Gtk.ResponseType.APPLY)
@ -784,40 +845,13 @@ class MyTimelineView(NavigationView):
updating_category = [False]
# Connect category checkbox to toggle all children
def make_category_toggle_handler(cb_list, updating_flag):
def handler(widget):
if updating_flag[0]:
return
# Handle inconsistent state - make it consistent
if widget.get_inconsistent():
widget.set_inconsistent(False)
widget.set_active(True)
updating_flag[0] = True
# Toggle all children
is_active = widget.get_active()
for child_cb in cb_list:
child_cb.set_active(is_active)
updating_flag[0] = False
return handler
category_checkbox.connect("toggled",
self._make_group_toggle_handler(child_checkboxes, updating_category))
# Connect child checkboxes to update category checkbox state
def make_child_toggle_handler(cat_cb, children, updating_flag):
def handler(widget):
if updating_flag[0]:
return
self._update_group_checkbox_state(cat_cb, children)
return handler
# Connect category checkbox
category_checkbox.connect("toggled",
make_category_toggle_handler(child_checkboxes, updating_category))
# Connect child checkboxes
for child_cb in child_checkboxes:
child_cb.connect("toggled",
make_child_toggle_handler(category_checkbox, child_checkboxes, updating_category))
self._make_child_toggle_handler(category_checkbox, child_checkboxes, updating_category))
self._filter_widgets['event_type_checkboxes'] = event_type_checkboxes
self._filter_widgets['category_checkboxes'] = category_checkboxes
@ -906,10 +940,10 @@ class MyTimelineView(NavigationView):
calendar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=15)
calendar_box.set_homogeneous(True)
# Year range for genealogical data (1000 to current year + 10)
# Year range for genealogical data
import datetime
current_year = datetime.date.today().year
min_year = 1000
min_year = MIN_GENEALOGICAL_YEAR
max_year = current_year + 10
# From date calendar with year selector
@ -1130,37 +1164,13 @@ class MyTimelineView(NavigationView):
updating_family = [False]
# Connect family checkbox to toggle all members
def make_family_toggle_handler(cb_list, updating_flag):
def handler(widget):
if updating_flag[0]:
return
# Handle inconsistent state
if widget.get_inconsistent():
widget.set_inconsistent(False)
widget.set_active(True)
updating_flag[0] = True
is_active = widget.get_active()
for child_cb in cb_list:
child_cb.set_active(is_active)
updating_flag[0] = False
return handler
family_checkbox.connect("toggled",
self._make_group_toggle_handler(child_checkboxes, updating_family))
# Connect child checkboxes to update family checkbox
def make_family_member_toggle_handler(fam_cb, children, updating_flag):
def handler(widget):
if updating_flag[0]:
return
self._update_group_checkbox_state(fam_cb, children)
return handler
# Connect family checkbox
family_checkbox.connect("toggled",
make_family_toggle_handler(child_checkboxes, updating_family))
# Connect child checkboxes
for child_cb in child_checkboxes:
child_cb.connect("toggled",
make_family_member_toggle_handler(family_checkbox, child_checkboxes, updating_family))
self._make_child_toggle_handler(family_checkbox, child_checkboxes, updating_family))
# Initialize family checkbox state
self._update_group_checkbox_state(family_checkbox, child_checkboxes)
@ -1182,9 +1192,9 @@ class MyTimelineView(NavigationView):
min_sort, max_sort = self.date_range_filter
# Convert sort values back to dates for calendar display
# Approximate conversion: extract year from sort value
# Sort value is roughly: year * 10000 + month * 100 + day
from_year = min_sort // 10000
to_year = max_sort // 10000
# Sort value is roughly: year * DATE_SORT_YEAR_MULTIPLIER + month * DATE_SORT_MONTH_MULTIPLIER + day
from_year = min_sort // DATE_SORT_YEAR_MULTIPLIER
to_year = max_sort // DATE_SORT_YEAR_MULTIPLIER
# Set calendar years (approximate)
current_from_year, current_from_month, current_from_day = from_calendar.get_date()
@ -1639,10 +1649,10 @@ class MyTimelineView(NavigationView):
)
self.timeline_height = int(base_height * self.zoom_level)
else:
self.timeline_height = int(600 * self.zoom_level)
self.timeline_height = int(EMPTY_TIMELINE_HEIGHT * self.zoom_level)
if self.drawing_area:
self.drawing_area.set_size_request(800, self.timeline_height)
self.drawing_area.set_size_request(DEFAULT_DRAWING_AREA_WIDTH, self.timeline_height)
def _create_timeline_event(self, event: 'Event', person_obj: Optional['Person'] = None, y_pos: float = 0.0) -> Optional[TimelineEvent]:
"""
@ -2280,10 +2290,10 @@ class MyTimelineView(NavigationView):
)
self.timeline_height = int(base_height * self.zoom_level)
else:
self.timeline_height = int(600 * self.zoom_level)
self.timeline_height = int(EMPTY_TIMELINE_HEIGHT * self.zoom_level)
if self.drawing_area:
self.drawing_area.set_size_request(800, self.timeline_height)
self.drawing_area.set_size_request(DEFAULT_DRAWING_AREA_WIDTH, self.timeline_height)
# Invalidate cache when zoom changes
self._adjusted_events_cache = None
@ -2570,7 +2580,7 @@ class MyTimelineView(NavigationView):
return None
def _format_person_tooltip(self, person, person_events: List[TimelineEvent]) -> str:
def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str:
"""
Format tooltip for person with multiple events.
@ -2607,7 +2617,7 @@ class MyTimelineView(NavigationView):
return tooltip_text
def _format_single_event_tooltip(self, event, event_type: EventType) -> str:
def _format_single_event_tooltip(self, event: 'Event', event_type: EventType) -> str:
"""
Format tooltip for single event.

View File

@ -4,14 +4,25 @@ Generate a huge demo family for Gramps testing
"""
import random
from datetime import datetime, timedelta
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, List, Tuple, Dict
# Set seed for deterministic event generation
random.seed(42)
# Generate unique handles
def gen_handle(prefix, num):
return f"_{prefix}{num:08d}"
# Constants
EVENT_ID_OFFSET = 10
FAMILY_ID_OFFSET = 100
EVENT_ID_START_OFFSET = 2
MIN_MONTH = 1
MAX_MONTH = 12
MIN_DAY = 1
MAX_DAY = 28
GRAMPS_XML_VERSION = "5.1.0"
GRAMPS_XML_NAMESPACE = "http://gramps-project.org/xml/1.7.1/"
GRAMPS_XML_DTD = "http://gramps-project.org/xml/1.7.1/grampsxml.dtd"
# Event types to add
EVENT_TYPES = [
@ -29,10 +40,115 @@ EVENT_TYPES = [
("Cremation", 0.2, None, None), # 20% chance if death exists, at death time
]
# Generate additional events for a person
def gen_additional_events(pid, first_name, surname, birth_year, death_year=None):
events = []
event_id_offset = pid * 10 + 2 # Start after birth and death events
# Name lists
MALE_NAMES = [
"James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph",
"Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven",
"Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "George", "Timothy",
"Ronald", "Jason", "Edward", "Jeffrey", "Ryan", "Jacob", "Gary", "Nicholas",
"Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", "Benjamin"
]
FEMALE_NAMES = [
"Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan",
"Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra",
"Ashley", "Kimberly", "Emily", "Donna", "Michelle", "Dorothy", "Carol",
"Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura",
"Cynthia", "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela",
"Emma", "Nicole", "Helen", "Samantha", "Katherine", "Christine", "Debra"
]
SURNAMES = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson", "Anderson", "Thomas",
"Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris",
"Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen",
"King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams"
]
OCCUPATIONS = [
"Farmer", "Teacher", "Engineer", "Doctor", "Lawyer", "Merchant",
"Carpenter", "Blacksmith", "Sailor", "Soldier", "Clerk", "Nurse"
]
PLACES = [
"New York", "London", "Paris", "Berlin", "Rome", "Madrid", "Amsterdam",
"Vienna", "Prague", "Warsaw", "Stockholm", "Copenhagen"
]
@dataclass
class EventData:
"""Data structure for an event."""
handle: str
event_type: str
year: int
month: int
day: int
description: str
event_id: int
@dataclass
class PersonData:
"""Data structure for person information."""
handle: str
name: str
surname: str
birth: int
death: Optional[int]
gender: str
parentin: List[str]
childof: List[str]
@dataclass
class FamilyData:
"""Data structure for family information."""
handle: str
father_handle: str
mother_handle: str
children_handles: List[str]
marriage_year: int
marriage_handle: str
family_id: int
def gen_handle(prefix: str, num: int) -> str:
"""Generate unique handle."""
return f"_{prefix}{num:08d}"
def create_event_element(event_data: EventData) -> ET.Element:
"""Create an XML element for an event."""
event_elem = ET.Element("event")
event_elem.set("handle", event_data.handle)
event_elem.set("change", str(int(datetime.now().timestamp())))
event_elem.set("id", f"E{event_data.event_id:04d}")
type_elem = ET.SubElement(event_elem, "type")
type_elem.text = event_data.event_type
date_elem = ET.SubElement(event_elem, "dateval")
date_elem.set("val", f"{event_data.year}-{event_data.month:02d}-{event_data.day:02d}")
if event_data.description:
desc_elem = ET.SubElement(event_elem, "description")
desc_elem.text = event_data.description
return event_elem
def gen_additional_events(
pid: int,
first_name: str,
surname: str,
birth_year: int,
death_year: Optional[int] = None
) -> List[Tuple[str, EventData]]:
"""Generate additional events for a person."""
events: List[Tuple[str, EventData]] = []
event_id_offset = pid * EVENT_ID_OFFSET + EVENT_ID_START_OFFSET
for event_type, probability, min_years, max_years in EVENT_TYPES:
if random.random() > probability:
@ -43,19 +159,18 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None)
if not death_year:
continue
event_year = death_year
event_month = random.randint(1, 12)
event_day = random.randint(1, 28)
event_month = random.randint(MIN_MONTH, MAX_MONTH)
event_day = random.randint(MIN_DAY, MAX_DAY)
else:
if max_years is None:
continue
event_year = birth_year + random.randint(min_years, max_years)
if death_year and event_year > death_year:
continue
event_month = random.randint(1, 12)
event_day = random.randint(1, 28)
event_month = random.randint(MIN_MONTH, MAX_MONTH)
event_day = random.randint(MIN_DAY, MAX_DAY)
event_handle = gen_handle("EVENT", event_id_offset)
event_id_offset += 1
# Generate description based on event type
if event_type == "Education":
@ -63,16 +178,12 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None)
elif event_type == "Graduation":
description = f"Graduation - {first_name} {surname}"
elif event_type == "Occupation":
occupations = ["Farmer", "Teacher", "Engineer", "Doctor", "Lawyer", "Merchant",
"Carpenter", "Blacksmith", "Sailor", "Soldier", "Clerk", "Nurse"]
occupation = random.choice(occupations)
occupation = random.choice(OCCUPATIONS)
description = f"{occupation} - {first_name} {surname}"
elif event_type == "Military Service":
description = f"Military Service - {first_name} {surname}"
elif event_type == "Residence":
places = ["New York", "London", "Paris", "Berlin", "Rome", "Madrid", "Amsterdam",
"Vienna", "Prague", "Warsaw", "Stockholm", "Copenhagen"]
place = random.choice(places)
place = random.choice(PLACES)
description = f"Residence in {place} - {first_name} {surname}"
elif event_type == "Emigration":
description = f"Emigration - {first_name} {surname}"
@ -83,136 +194,218 @@ def gen_additional_events(pid, first_name, surname, birth_year, death_year=None)
else:
description = f"{event_type} of {surname}, {first_name}"
event_xml = f""" <event handle="{event_handle}" change="{int(datetime.now().timestamp())}" id="E{event_id_offset-1:04d}">
<type>{event_type}</type>
<dateval val="{event_year}-{event_month:02d}-{event_day:02d}"/>
<description>{description}</description>
</event>
"""
events.append((event_handle, event_xml))
event_data = EventData(
handle=event_handle,
event_type=event_type,
year=event_year,
month=event_month,
day=event_day,
description=description,
event_id=event_id_offset
)
events.append((event_handle, event_data))
event_id_offset += 1
return events
# Generate a person
def gen_person(pid, first_name, surname, birth_year, death_year=None, gender="M",
parentin_families=None, childof_families=None, reuse_additional_events=None):
def gen_person(
pid: int,
first_name: str,
surname: str,
birth_year: int,
death_year: Optional[int] = None,
gender: str = "M",
parentin_families: Optional[List[str]] = None,
childof_families: Optional[List[str]] = None,
reuse_additional_events: Optional[List[Tuple[str, EventData]]] = None
) -> Tuple[ET.Element, ET.Element, Optional[ET.Element], List[ET.Element], List[Tuple[str, EventData]]]:
"""Generate a person with all associated events."""
handle = gen_handle("PERSON", pid)
birth_handle = gen_handle("EVENT", pid * 10)
death_handle = gen_handle("EVENT", pid * 10 + 1) if death_year else None
birth_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET)
death_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET + 1) if death_year else None
person_xml = f""" <person handle="{handle}" change="{int(datetime.now().timestamp())}" id="I{pid:04d}">
<gender>{gender}</gender>
<name type="Birth Name">
<first>{first_name}</first>
<surname>{surname}</surname>
</name>
<eventref hlink="{birth_handle}" role="Primary"/>
"""
# Create person element
person_elem = ET.Element("person")
person_elem.set("handle", handle)
person_elem.set("change", str(int(datetime.now().timestamp())))
person_elem.set("id", f"I{pid:04d}")
gender_elem = ET.SubElement(person_elem, "gender")
gender_elem.text = gender
name_elem = ET.SubElement(person_elem, "name")
name_elem.set("type", "Birth Name")
first_elem = ET.SubElement(name_elem, "first")
first_elem.text = first_name
surname_elem = ET.SubElement(name_elem, "surname")
surname_elem.text = surname
# Birth event reference
birth_ref = ET.SubElement(person_elem, "eventref")
birth_ref.set("hlink", birth_handle)
birth_ref.set("role", "Primary")
# Death event reference
if death_handle:
person_xml += f""" <eventref hlink="{death_handle}" role="Primary"/>
"""
death_ref = ET.SubElement(person_elem, "eventref")
death_ref.set("hlink", death_handle)
death_ref.set("role", "Primary")
# Add additional events - reuse if provided, otherwise generate new
if reuse_additional_events is not None:
# reuse_additional_events is a list of (handle, xml) tuples
additional_events = reuse_additional_events
else:
additional_events = gen_additional_events(pid, first_name, surname, birth_year, death_year)
for event_handle, _ in additional_events:
person_xml += f""" <eventref hlink="{event_handle}" role="Primary"/>
"""
event_ref = ET.SubElement(person_elem, "eventref")
event_ref.set("hlink", event_handle)
event_ref.set("role", "Primary")
# Add parentin references (for fathers and mothers)
# Add parentin references
if parentin_families:
for family_handle in parentin_families:
person_xml += f""" <parentin hlink="{family_handle}"/>
"""
# Add childof references (for children)
parentin_elem = ET.SubElement(person_elem, "parentin")
parentin_elem.set("hlink", family_handle)
# Add childof references
if childof_families:
for family_handle in childof_families:
person_xml += f""" <childof hlink="{family_handle}"/>
"""
person_xml += """ </person>
"""
childof_elem = ET.SubElement(person_elem, "childof")
childof_elem.set("hlink", family_handle)
# Birth event
birth_month = random.randint(1, 12)
birth_day = random.randint(1, 28)
birth_event = f""" <event handle="{birth_handle}" change="{int(datetime.now().timestamp())}" id="E{pid*10:04d}">
<type>Birth</type>
<dateval val="{birth_year}-{birth_month:02d}-{birth_day:02d}"/>
<description>Birth of {surname}, {first_name}</description>
</event>
"""
birth_month = random.randint(MIN_MONTH, MAX_MONTH)
birth_day = random.randint(MIN_DAY, MAX_DAY)
birth_event_data = EventData(
handle=birth_handle,
event_type="Birth",
year=birth_year,
month=birth_month,
day=birth_day,
description=f"Birth of {surname}, {first_name}",
event_id=pid * EVENT_ID_OFFSET
)
birth_event = create_event_element(birth_event_data)
# Death event
death_event = ""
death_event: Optional[ET.Element] = None
if death_handle and death_year:
death_month = random.randint(1, 12)
death_day = random.randint(1, 28)
death_event = f""" <event handle="{death_handle}" change="{int(datetime.now().timestamp())}" id="E{pid*10+1:04d}">
<type>Death</type>
<dateval val="{death_year}-{death_month:02d}-{death_day:02d}"/>
<description>Death of {surname}, {first_name}</description>
</event>
"""
death_month = random.randint(MIN_MONTH, MAX_MONTH)
death_day = random.randint(MIN_DAY, MAX_DAY)
death_event_data = EventData(
handle=death_handle,
event_type="Death",
year=death_year,
month=death_month,
day=death_day,
description=f"Death of {surname}, {first_name}",
event_id=pid * EVENT_ID_OFFSET + 1
)
death_event = create_event_element(death_event_data)
# Collect all additional events (return tuples for reuse, XML strings for output)
all_additional_events_xml = [event_xml for _, event_xml in additional_events]
# Convert additional events to XML elements
all_additional_events_xml = [create_event_element(event_data) for _, event_data in additional_events]
return person_xml, birth_event, death_event, all_additional_events_xml, additional_events
return person_elem, birth_event, death_event, all_additional_events_xml, additional_events
# Generate a family
def gen_family(fid, father_handle, mother_handle, marriage_year, children_handles):
def gen_family(
fid: int,
father_handle: str,
mother_handle: str,
marriage_year: int,
children_handles: List[str]
) -> Tuple[ET.Element, ET.Element]:
"""Generate a family with marriage event."""
handle = gen_handle("FAMILY", fid)
marriage_handle = gen_handle("EVENT", fid * 100)
marriage_handle = gen_handle("EVENT", fid * FAMILY_ID_OFFSET)
# Create family element
family_elem = ET.Element("family")
family_elem.set("handle", handle)
family_elem.set("change", str(int(datetime.now().timestamp())))
family_elem.set("id", f"F{fid:04d}")
rel_elem = ET.SubElement(family_elem, "rel")
rel_elem.set("type", "Married")
father_elem = ET.SubElement(family_elem, "father")
father_elem.set("hlink", father_handle)
mother_elem = ET.SubElement(family_elem, "mother")
mother_elem.set("hlink", mother_handle)
family_xml = f""" <family handle="{handle}" change="{int(datetime.now().timestamp())}" id="F{fid:04d}">
<rel type="Married"/>
<father hlink="{father_handle}"/>
<mother hlink="{mother_handle}"/>
"""
for child_handle in children_handles:
family_xml += f""" <childref hlink="{child_handle}"/>
"""
family_xml += f""" <eventref hlink="{marriage_handle}" role="Family"/>
</family>
"""
child_elem = ET.SubElement(family_elem, "childref")
child_elem.set("hlink", child_handle)
marriage_ref = ET.SubElement(family_elem, "eventref")
marriage_ref.set("hlink", marriage_handle)
marriage_ref.set("role", "Family")
# Marriage event
marriage_month = random.randint(1, 12)
marriage_day = random.randint(1, 28)
marriage_event = f""" <event handle="{marriage_handle}" change="{int(datetime.now().timestamp())}" id="E{fid*100:04d}">
<type>Marriage</type>
<dateval val="{marriage_year}-{marriage_month:02d}-{marriage_day:02d}"/>
<description>Marriage</description>
</event>
"""
marriage_month = random.randint(MIN_MONTH, MAX_MONTH)
marriage_day = random.randint(MIN_DAY, MAX_DAY)
marriage_event_data = EventData(
handle=marriage_handle,
event_type="Marriage",
year=marriage_year,
month=marriage_month,
day=marriage_day,
description="Marriage",
event_id=fid * FAMILY_ID_OFFSET
)
marriage_event = create_event_element(marriage_event_data)
return family_xml, marriage_event
return family_elem, marriage_event
# First names
male_names = ["James", "John", "Robert", "Michael", "William", "David", "Richard", "Joseph",
"Thomas", "Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven",
"Paul", "Andrew", "Joshua", "Kenneth", "Kevin", "Brian", "George", "Timothy",
"Ronald", "Jason", "Edward", "Jeffrey", "Ryan", "Jacob", "Gary", "Nicholas",
"Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon", "Benjamin"]
female_names = ["Mary", "Patricia", "Jennifer", "Linda", "Elizabeth", "Barbara", "Susan",
"Jessica", "Sarah", "Karen", "Nancy", "Lisa", "Betty", "Margaret", "Sandra",
"Ashley", "Kimberly", "Emily", "Donna", "Michelle", "Dorothy", "Carol",
"Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura",
"Cynthia", "Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela",
"Emma", "Nicole", "Helen", "Samantha", "Katherine", "Christine", "Debra"]
def create_gramps_xml_document(
events: List[ET.Element],
people: List[ET.Element],
families: List[ET.Element]
) -> ET.ElementTree:
"""Create the complete Gramps XML document."""
# Create root element
database = ET.Element("database")
database.set("xmlns", GRAMPS_XML_NAMESPACE)
# Header
header = ET.SubElement(database, "header")
created = ET.SubElement(header, "created")
created.set("date", datetime.now().strftime('%Y-%m-%d'))
created.set("version", GRAMPS_XML_VERSION)
researcher = ET.SubElement(header, "researcher")
resname = ET.SubElement(researcher, "resname")
resname.text = "Demo Family Generator"
# Tags (empty)
ET.SubElement(database, "tags")
# Events
events_elem = ET.SubElement(database, "events")
for event in events:
events_elem.append(event)
# People
people_elem = ET.SubElement(database, "people")
for person in people:
people_elem.append(person)
# Families
families_elem = ET.SubElement(database, "families")
for family in families:
families_elem.append(family)
return ET.ElementTree(database)
surnames = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis",
"Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson", "Anderson", "Thomas",
"Taylor", "Moore", "Jackson", "Martin", "Lee", "Thompson", "White", "Harris",
"Sanchez", "Clark", "Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen",
"King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green", "Adams"]
def main():
def main() -> None:
"""Main function to generate the demo family."""
print("Generating huge demo family...")
# Generate main family
@ -221,7 +414,7 @@ def main():
father_handle = gen_handle("PERSON", father_id)
main_family_handle = gen_handle("FAMILY", 1)
father_person, father_birth, father_death, father_additional_xml, _ = gen_person(
father_id, "John", "Smith", 1950, 2010, "M",
father_id, "John", "Smith", 1950, 2010, "M",
parentin_families=[main_family_handle]
)
@ -234,17 +427,21 @@ def main():
)
all_additional_events = father_additional_xml + mother_additional_xml
all_events = [father_birth, mother_birth]
if father_death:
all_events.append(father_death)
if mother_death:
all_events.append(mother_death)
# Generate 15 children
children = []
child_handles = []
child_events = []
child_additional_events_map = {} # Store additional events by child_id
children: List[ET.Element] = []
child_handles: List[str] = []
child_additional_events_map: Dict[int, List[Tuple[str, EventData]]] = {}
child_id = 3
for i in range(15):
gender = "M" if i % 2 == 0 else "F"
first_name = random.choice(male_names if gender == "M" else female_names)
first_name = random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES)
birth_year = 1970 + (i * 2) # Spread births from 1970 to 1998
death_year = birth_year + random.randint(60, 90) if random.random() < 0.3 else None # 30% chance of death
@ -256,9 +453,9 @@ def main():
children.append(child_person)
child_handles.append(child_handle)
child_events.append(child_birth)
all_events.append(child_birth)
if child_death:
child_events.append(child_death)
all_events.append(child_death)
# Store tuples for reuse when regenerating
child_additional_events_map[child_id] = child_additional_tuples
all_additional_events.extend(child_additional_xml)
@ -266,40 +463,66 @@ def main():
# Generate family
family_id = 1
family_xml, marriage_event = gen_family(family_id, father_handle, mother_handle, 1969, child_handles)
family_elem, marriage_event = gen_family(family_id, father_handle, mother_handle, 1969, child_handles)
all_events.append(marriage_event)
families: List[ET.Element] = [family_elem]
# Track person data for regeneration (needed for children who become parents)
import re
person_data = {}
person_data: Dict[int, PersonData] = {}
# Store initial person data
person_data[father_id] = {"handle": father_handle, "name": "John", "surname": "Smith",
"birth": 1950, "death": 2010, "gender": "M",
"parentin": [main_family_handle], "childof": []}
person_data[mother_id] = {"handle": mother_handle, "name": "Mary", "surname": "Smith",
"birth": 1952, "death": 2015, "gender": "F",
"parentin": [main_family_handle], "childof": []}
person_data[father_id] = PersonData(
handle=father_handle,
name="John",
surname="Smith",
birth=1950,
death=2010,
gender="M",
parentin=[main_family_handle],
childof=[]
)
person_data[mother_id] = PersonData(
handle=mother_handle,
name="Mary",
surname="Smith",
birth=1952,
death=2015,
gender="F",
parentin=[main_family_handle],
childof=[]
)
for i, child_handle in enumerate(child_handles):
child_pid = 3 + i
gender = "M" if i % 2 == 0 else "F"
# Extract name from generated child XML
child_xml = children[i]
name_match = re.search(r'<first>([^<]+)</first>', child_xml)
first_name = name_match.group(1) if name_match else random.choice(male_names if gender == "M" else female_names)
name_elem = children[i].find(".//first")
first_name = name_elem.text if name_elem is not None and name_elem.text else random.choice(MALE_NAMES if gender == "M" else FEMALE_NAMES)
birth_year = 1970 + (i * 2)
# Extract death year from child_events if it exists
# Extract death year from events if it exists
death_year = None
for event in child_events:
if f"id=\"E{child_pid*10+1:04d}\"" in event:
match = re.search(r'val="(\d{4})', event)
if match:
death_year = int(match.group(1))
person_data[child_pid] = {"handle": child_handle, "name": first_name, "surname": "Smith",
"birth": birth_year, "death": death_year, "gender": gender,
"parentin": [], "childof": [main_family_handle]}
for event in all_events:
if event.get("id") == f"E{child_pid * EVENT_ID_OFFSET + 1:04d}":
date_elem = event.find(".//dateval")
if date_elem is not None:
date_val = date_elem.get("val", "")
if date_val:
try:
death_year = int(date_val.split("-")[0])
except (ValueError, IndexError):
pass
person_data[child_pid] = PersonData(
handle=child_handle,
name=first_name,
surname="Smith",
birth=birth_year,
death=death_year,
gender=gender,
parentin=[],
childof=[main_family_handle]
)
# Generate grandchildren (children of first 5 children)
grandchildren = []
grandchild_events = []
grandchildren: List[ET.Element] = []
grandchild_id = child_id
for i in range(5): # First 5 children have children
@ -309,41 +532,55 @@ def main():
spouse_gender = "F" if parent_gender == "M" else "M"
# Create spouse
spouse_name = random.choice(female_names if spouse_gender == "F" else male_names)
spouse_name = random.choice(FEMALE_NAMES if spouse_gender == "F" else MALE_NAMES)
spouse_birth = 1970 + (i * 2) + random.randint(-2, 2)
spouse_handle = gen_handle("PERSON", grandchild_id)
child_family_handle = gen_handle("FAMILY", family_id + 1)
person_data[grandchild_id] = {"handle": spouse_handle, "name": spouse_name, "surname": "Smith",
"birth": spouse_birth, "death": None, "gender": spouse_gender,
"parentin": [child_family_handle], "childof": []}
person_data[grandchild_id] = PersonData(
handle=spouse_handle,
name=spouse_name,
surname="Smith",
birth=spouse_birth,
death=None,
gender=spouse_gender,
parentin=[child_family_handle],
childof=[]
)
spouse_person, spouse_birth_event, spouse_death_event, spouse_additional_xml, _ = gen_person(
grandchild_id, spouse_name, "Smith", spouse_birth, None, spouse_gender,
parentin_families=[child_family_handle]
)
grandchildren.append(spouse_person)
grandchild_events.append(spouse_birth_event)
all_events.append(spouse_birth_event)
if spouse_death_event:
grandchild_events.append(spouse_death_event)
all_events.append(spouse_death_event)
all_additional_events.extend(spouse_additional_xml)
grandchild_id += 1
# Update parent to include parentin reference
person_data[parent_pid]["parentin"].append(child_family_handle)
person_data[parent_pid].parentin.append(child_family_handle)
# Create 3-5 children per couple
num_grandchildren = random.randint(3, 5)
grandchild_handles = []
grandchild_handles: List[str] = []
for j in range(num_grandchildren):
gchild_gender = "M" if j % 2 == 0 else "F"
gchild_name = random.choice(male_names if gchild_gender == "M" else female_names)
gchild_name = random.choice(MALE_NAMES if gchild_gender == "M" else FEMALE_NAMES)
gchild_birth = 1995 + (i * 3) + j
gchild_handle = gen_handle("PERSON", grandchild_id)
person_data[grandchild_id] = {"handle": gchild_handle, "name": gchild_name, "surname": "Smith",
"birth": gchild_birth, "death": None, "gender": gchild_gender,
"parentin": [], "childof": [child_family_handle]}
person_data[grandchild_id] = PersonData(
handle=gchild_handle,
name=gchild_name,
surname="Smith",
birth=gchild_birth,
death=None,
gender=gchild_gender,
parentin=[],
childof=[child_family_handle]
)
gchild_person, gchild_birth_event, gchild_death_event, gchild_additional_xml, _ = gen_person(
grandchild_id, gchild_name, "Smith", gchild_birth, None, gchild_gender,
@ -351,17 +588,17 @@ def main():
)
grandchildren.append(gchild_person)
grandchild_handles.append(gchild_handle)
grandchild_events.append(gchild_birth_event)
all_events.append(gchild_birth_event)
if gchild_death_event:
grandchild_events.append(gchild_death_event)
all_events.append(gchild_death_event)
all_additional_events.extend(gchild_additional_xml)
grandchild_id += 1
# Create family for this couple
family_id += 1
fam_xml, fam_marriage = gen_family(family_id, parent_handle, spouse_handle, 1990 + i, grandchild_handles)
family_xml += fam_xml
child_events.append(fam_marriage)
fam_elem, fam_marriage = gen_family(family_id, parent_handle, spouse_handle, 1990 + i, grandchild_handles)
families.append(fam_elem)
all_events.append(fam_marriage)
# Regenerate children XMLs with updated family references
# We need to regenerate to update family references, but reuse the same events
@ -372,63 +609,41 @@ def main():
# Reuse the original additional events to ensure consistency
original_additional_events = child_additional_events_map.get(child_pid, [])
child_person, _, _, _, _ = gen_person(
child_pid, data["name"], data["surname"], data["birth"], data["death"], data["gender"],
parentin_families=data["parentin"], childof_families=data["childof"],
child_pid, data.name, data.surname, data.birth, data.death, data.gender,
parentin_families=data.parentin, childof_families=data.childof,
reuse_additional_events=original_additional_events
)
children.append(child_person)
# Write XML file
xml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.1//EN"
"http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
<database xmlns="http://gramps-project.org/xml/1.7.1/">
<header>
<created date="{datetime.now().strftime('%Y-%m-%d')}" version="5.1.0"/>
<researcher>
<resname>Demo Family Generator</resname>
</researcher>
</header>
<tags>
</tags>
<events>
{father_birth}
{father_death}
{mother_birth}
{mother_death}
{marriage_event}
"""
# Add all additional events to events list
all_events.extend(all_additional_events)
for event in child_events:
xml_content += event
for event in grandchild_events:
xml_content += event
for event in all_additional_events:
xml_content += event
# Create complete XML document
people = [father_person, mother_person] + children + grandchildren
tree = create_gramps_xml_document(all_events, people, families)
xml_content += """ </events>
<people>
"""
xml_content += father_person
xml_content += mother_person
for child in children:
xml_content += child
for grandchild in grandchildren:
xml_content += grandchild
# Write XML file with proper formatting
# ET.indent is only available in Python 3.9+, so we'll format manually if needed
try:
ET.indent(tree, space=" ")
except AttributeError:
# Python < 3.9 doesn't have indent, will write without indentation
pass
tree.write("demo_family.gramps", encoding="utf-8", xml_declaration=True)
xml_content += """ </people>
<families>
"""
xml_content += family_xml
# Add DOCTYPE declaration (ElementTree doesn't support this directly)
with open("demo_family.gramps", "r", encoding="utf-8") as f:
content = f.read()
xml_content += """ </families>
</database>
"""
# Insert DOCTYPE after XML declaration
doctype = f'<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.1//EN"\n"{GRAMPS_XML_DTD}">\n'
content = content.replace('<?xml version="1.0" encoding="utf-8"?>',
f'<?xml version="1.0" encoding="UTF-8"?>\n{doctype}', 1)
with open("demo_family.gramps", "w", encoding="utf-8") as f:
f.write(xml_content)
f.write(content)
total_events = len(child_events) + len(grandchild_events) + len(all_additional_events)
total_events = len(all_events)
print(f"Generated demo_family.gramps with:")
print(f" - 2 parents (John and Mary Smith)")
print(f" - 15 children")
@ -439,6 +654,6 @@ def main():
print(f" - {len(all_additional_events)} additional events (Baptism, Education, Occupation, etc.)")
print(f" - Total events: {total_events}")
if __name__ == "__main__":
main()

View File

@ -6,12 +6,13 @@
# so it can be used in the snap-installed version of Gramps 6.0
#
set -e # Exit on error
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Get the directory where this script is located
@ -40,6 +41,22 @@ echo "MyTimeline Plugin Installer for Snap Gramps"
echo "=========================================="
echo ""
# Function to check if Gramps is installed via snap
check_gramps_installed() {
if ! command -v gramps &> /dev/null && ! snap list gramps &> /dev/null; then
echo -e "${YELLOW}Warning: Gramps may not be installed via snap.${NC}"
echo "This script is designed for snap-installed Gramps."
echo "If Gramps is installed differently, you may need to manually install the plugin."
echo ""
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation cancelled."
exit 0
fi
fi
}
# Function to find the correct plugin directory
find_plugin_dir() {
# Try the primary location first
@ -65,6 +82,9 @@ find_plugin_dir() {
return 0
}
# Check if Gramps is installed
check_gramps_installed
# Verify source files exist
echo "Checking source files..."
MISSING_FILES=()
@ -82,6 +102,11 @@ if [ ${#MISSING_FILES[@]} -gt 0 ]; then
for file in "${MISSING_FILES[@]}"; do
echo " - $file"
done
echo ""
echo -e "${YELLOW}Troubleshooting:${NC}"
echo " 1. Make sure you're running this script from the directory containing the plugin files"
echo " 2. Verify that MyTimeline.gpr.py and MyTimeline.py exist in: $PLUGIN_SRC_DIR"
echo " 3. Check file permissions: ls -la $PLUGIN_SRC_DIR"
exit 1
fi
@ -92,7 +117,14 @@ TARGET_DIR=$(find_plugin_dir)
if [ -z "$TARGET_DIR" ]; then
echo -e "${RED}Error: Could not determine plugin directory location.${NC}"
echo "Please check if Gramps is installed via snap."
echo ""
echo -e "${YELLOW}Troubleshooting:${NC}"
echo " 1. Verify Gramps is installed: snap list gramps"
echo " 2. Check if snap directory exists: ls -la $HOME/snap/gramps/"
echo " 3. Try running Gramps at least once to create the plugin directory"
echo " 4. Manually create the directory: mkdir -p $PLUGIN_DEST_DIR"
echo ""
echo "If the issue persists, you may need to install the plugin manually."
exit 1
fi
@ -126,7 +158,11 @@ fi
# Copy plugin files
echo "Installing plugin files..."
for file in "${PLUGIN_FILES[@]}"; do
cp "$PLUGIN_SRC_DIR/$file" "$TARGET_DIR/"
if ! cp "$PLUGIN_SRC_DIR/$file" "$TARGET_DIR/"; then
echo -e "${RED}Error: Failed to copy $file${NC}"
echo "Check file permissions and disk space."
exit 1
fi
echo -e "${GREEN}${NC} Installed: $file"
done

View File

@ -5,12 +5,13 @@
# This script removes the MyTimeline plugin files from the Gramps snap plugin directory
#
set -e # Exit on error
set -euo pipefail # Exit on error, undefined vars, pipe failures
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Plugin files to remove
@ -60,6 +61,12 @@ TARGET_DIR=$(find_plugin_dir)
if [ -z "$TARGET_DIR" ]; then
echo -e "${YELLOW}Warning: Plugin directory not found.${NC}"
echo "The plugin may already be uninstalled or Gramps may not be installed via snap."
echo ""
echo -e "${BLUE}Note:${NC} If you installed the plugin manually, you may need to remove it manually."
echo "Common locations to check:"
for dir in "${ALTERNATIVE_DIRS[@]}"; do
echo " - $dir"
done
exit 0
fi
@ -88,7 +95,11 @@ echo ""
echo "Removing plugin files..."
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$TARGET_DIR/$file" ]; then
rm "$TARGET_DIR/$file"
if ! rm "$TARGET_DIR/$file"; then
echo -e "${RED}Error: Failed to remove $file${NC}"
echo "Check file permissions."
exit 1
fi
echo -e "${GREEN}${NC} Removed: $file"
FILES_REMOVED=$((FILES_REMOVED + 1))
fi