diff --git a/MyTimeline.py b/MyTimeline.py
index d03fafa..5725dbd 100644
--- a/MyTimeline.py
+++ b/MyTimeline.py
@@ -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.
diff --git a/generate_demo_family.py b/generate_demo_family.py
index 114cc0d..b23e153 100644
--- a/generate_demo_family.py
+++ b/generate_demo_family.py
@@ -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_type}
-
- {description}
-
-"""
- 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"""
- {gender}
-
- {first_name}
- {surname}
-
-
-"""
+ # 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"""
-"""
+ 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"""
-"""
+ 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"""
-"""
- # 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"""
-"""
- person_xml += """
-"""
+ 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"""
- Birth
-
- Birth of {surname}, {first_name}
-
-"""
+ 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"""
- Death
-
- Death of {surname}, {first_name}
-
-"""
+ 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"""
-
-
-
-"""
for child_handle in children_handles:
- family_xml += f"""
-"""
- family_xml += f"""
-
-"""
+ 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"""
- Marriage
-
- Marriage
-
-"""
+ 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'([^<]+)', 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"""
-
-
-
-
-
- Demo Family Generator
-
-
-
-
-
-{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 += """
-
-"""
- 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 += """
-
-"""
- 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 += """
-
-"""
+ # Insert DOCTYPE after XML declaration
+ doctype = f'\n'
+ content = content.replace('',
+ f'\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()
-
diff --git a/install_to_snap.sh b/install_to_snap.sh
index 36c9b84..a83f078 100755
--- a/install_to_snap.sh
+++ b/install_to_snap.sh
@@ -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
diff --git a/uninstall_from_snap.sh b/uninstall_from_snap.sh
index 2601f0e..73b5225 100755
--- a/uninstall_from_snap.sh
+++ b/uninstall_from_snap.sh
@@ -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