Compare commits

...

8 Commits

Author SHA1 Message Date
5860b3d25c 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
2025-11-29 22:49:16 +01:00
27de315514 Improve type hints using TYPE_CHECKING pattern
Add TYPE_CHECKING imports and conditional imports for Gramps types:
- Added TYPE_CHECKING to typing imports
- Added conditional imports for Event, Person, Family under TYPE_CHECKING block
- No runtime overhead, only used for type checking

Update TimelineEvent dataclass type hints:
- Changed event: 'Any' to event: 'Event'
- Changed person: Optional['Any'] to person: Optional['Person']
- Maintains forward references with string literals

Update all method parameter type hints:
- _create_timeline_event(): event: 'Event', person_obj: Optional['Person']
- _process_event(): event: 'Event', person_obj: Optional['Person']
- _find_person_for_event(): event: 'Event' -> Optional['Person']
- _get_event_label_text(): event: 'Event', person: Optional['Person']
- _person_matches_handle(): person: Optional['Person']
- _get_family_display_name(): family: 'Family'
- draw_event_label(): event: 'Event', person: Optional['Person']

Update cache dictionary type hint:
- _event_to_person_cache: Dict[str, Optional[Any]] -> Dict[str, Optional['Person']]

Results:
- All Optional[Any] instances replaced with specific Gramps types
- Better IDE autocomplete and type checking support
- Improved code documentation through type hints
- No runtime overhead (TYPE_CHECKING is False at runtime)
- Maintains backward compatibility
2025-11-29 22:40:03 +01:00
3652246bd4 Major code improvements: performance, maintainability, and cleanup
Performance optimizations:
- Optimized event-to-person lookup from O(n*m) to O(n+m) using reverse index
- Added _build_event_to_person_index() to build cache once during event collection
- Improved collision detection algorithm efficiency

Code quality improvements:
- Removed duplicate logger definition
- Removed redundant checks in _calculate_max_connection_line_x()
- Removed unused wrapper method detect_label_overlaps()
- Removed 3 unused methods: _collect_person_events, _collect_person_event_refs, _process_event_ref (67 lines of dead code)

Code organization:
- Extracted helper methods to reduce duplication:
  * _get_person_display_name() for person name lookups
  * _create_timeline_event() for TimelineEvent creation
  * _copy_timeline_event_with_y_pos() for TimelineEvent copying
- Reduced code duplication in filter dialog with nested helper function
- Extracted magic numbers to constants (color values, spacing)

Maintainability:
- Better separation of concerns with helper methods
- Improved code organization and readability
- Consistent error handling patterns
- Fixed indentation issue in _get_person_color()

All changes are backward compatible and maintain existing functionality.
2025-11-29 22:32:56 +01:00
66c932e4b8 Make timeline X position dynamic based on connection lines
- Added TIMELINE_LEFT_SPACING constant for spacing between connection lines and timeline
- Added _calculate_max_connection_line_x() to find rightmost connection line position
- Added _calculate_timeline_x() to dynamically calculate timeline X position
- Timeline now adjusts based on number of selected persons and their connection lines
- Updated on_draw() and find_event_at_position() to use dynamic timeline_x
- Timeline maintains minimum position to prevent overlap with year labels
- Event symbols and text now position relative to connection lines on the left
2025-11-29 22:20:49 +01:00
f6069dfd83 Add multiple person selection with colored connection lines
- Changed from single selected_person_handle to selected_person_handles set
- Added color generation using HSV color space for distinct person colors
- Added unique X positions for each person's vertical connection line
- Updated click handler to toggle persons in/out of selection set
- Refactored draw_person_connections to draw separate colored lines for each person
- Each selected person gets their own colored vertical line at unique X position
- Colors are consistent for each person based on handle hash
2025-11-29 22:10:25 +01:00
c9f6e7f8b8 Add three-state toggles to filter dialog groups
- Added helper methods for three-state checkbox management
  (_calculate_group_state, _update_group_checkbox_state)
- Replaced category labels with three-state checkboxes in Event Types tab
- Added three-state checkboxes to family expander labels in Persons tab
- Implemented bidirectional state synchronization between group and child checkboxes
- Group checkboxes show checked (all), unchecked (none), or inconsistent (some) states
2025-11-29 21:58:35 +01:00
ce75cd55bb Convert plugin to Event-based view and improve filter dialog
- Convert plugin from Family-based to Event-based view
  * Change category from Families to Events
  * Update navigation_type to 'Event'
  * Replace FamilyBookmarks with EventBookmarks
  * Rewrite collect_events() to show all events in database
  * Update goto_handle() to work with event handles

- Update filter dialog to show families with members
  * Restructure person filter page with expandable families
  * Each family shows father, mother, and children
  * Add helper method to generate family display names

- Restore person connection lines
  * Re-enable visual connections between events of selected person
  * Clicking an event selects the person and shows connections

- Add uninstall script
  * Remove plugin files and backup directories
  * Clean up any plugin files in subdirectories
2025-11-29 21:42:10 +01:00
b32be12aea Update plugin to Gramps 6.0 and add snap installation script
- Update MODULE_VERSION from 5.1 to 6.0 in MyTimeline.gpr.py
- Add install_to_snap.sh script to copy plugin to snap-installed Gramps
- Script includes automatic directory detection, backup functionality, and error handling
2025-11-29 20:50:43 +01:00
5 changed files with 1502 additions and 548 deletions

View File

@ -22,7 +22,7 @@ from gramps.gen.const import GRAMPS_LOCALE as glocale
_ = glocale.translation.gettext
MODULE_VERSION = "5.1"
MODULE_VERSION = "6.0"
# ------------------------------------------------------------------------
#
@ -34,14 +34,14 @@ register(
VIEW,
id="mytimelineview",
name=_("MyTimeline"),
description=_("A vertical timeline view showing family events including birth, death, and marriage"),
description=_("A vertical timeline view showing all events in the database"),
version="1.0",
gramps_target_version=MODULE_VERSION,
status=STABLE,
fname="MyTimeline.py",
authors=["Daniel Viegas"],
authors_email=["dlviegas@gmail.com"],
category=("Families", _("Families")),
category=("Events", _("Events")),
viewclass="MyTimelineView",
)

File diff suppressed because it is too large Load Diff

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):
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
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"/>
"""
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 * EVENT_ID_OFFSET)
death_handle = gen_handle("EVENT", pid * EVENT_ID_OFFSET + 1) if death_year else None
# 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)
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"]
# 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)
def main():
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)
def main() -> None:
"""Main function to generate the demo family."""
print("Generating huge demo family...")
# Generate main family
@ -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()

182
install_to_snap.sh Executable file
View File

@ -0,0 +1,182 @@
#!/bin/bash
#
# Script to install MyTimeline plugin to snap-installed Gramps
#
# This script copies the MyTimeline plugin files to the Gramps snap plugin directory
# so it can be used in the snap-installed version of Gramps 6.0
#
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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source plugin files
PLUGIN_SRC_DIR="$SCRIPT_DIR"
PLUGIN_FILES=("MyTimeline.gpr.py" "MyTimeline.py")
# Target directory for snap-installed Gramps
# Gramps 6.0 plugins directory in snap
PLUGIN_DEST_DIR="$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
# Alternative locations to try (in case snap structure differs)
ALTERNATIVE_DIRS=(
"$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
"$HOME/snap/gramps/current/.gramps/gramps60/plugins"
"$HOME/snap/gramps/current/.gramps/plugins"
"$HOME/snap/gramps/common/.local/share/gramps/gramps60/plugins"
"$HOME/.local/share/gramps/gramps60/plugins"
"$HOME/.gramps/gramps60/plugins"
)
echo "=========================================="
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
if [ -d "$PLUGIN_DEST_DIR" ]; then
echo "$PLUGIN_DEST_DIR"
return 0
fi
# Try alternative locations
for dir in "${ALTERNATIVE_DIRS[@]}"; do
if [ -d "$dir" ]; then
echo "$dir"
return 0
fi
done
# If not found, ask user or create the primary location
echo ""
echo -e "${YELLOW}Warning: Plugin directory not found in standard locations.${NC}"
echo "Attempting to create: $PLUGIN_DEST_DIR"
mkdir -p "$PLUGIN_DEST_DIR"
echo "$PLUGIN_DEST_DIR"
return 0
}
# Check if Gramps is installed
check_gramps_installed
# Verify source files exist
echo "Checking source files..."
MISSING_FILES=()
for file in "${PLUGIN_FILES[@]}"; do
if [ ! -f "$PLUGIN_SRC_DIR/$file" ]; then
MISSING_FILES+=("$file")
else
echo -e "${GREEN}${NC} Found: $file"
fi
done
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
echo ""
echo -e "${RED}Error: Missing required plugin files:${NC}"
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
# Find the target directory
echo ""
echo "Locating Gramps plugin directory..."
TARGET_DIR=$(find_plugin_dir)
if [ -z "$TARGET_DIR" ]; then
echo -e "${RED}Error: Could not determine plugin directory location.${NC}"
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
echo -e "${GREEN}${NC} Target directory: $TARGET_DIR"
echo ""
# Check if plugin already exists
PLUGIN_EXISTS=false
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$TARGET_DIR/$file" ]; then
PLUGIN_EXISTS=true
break
fi
done
# Backup existing plugin if it exists
if [ "$PLUGIN_EXISTS" = true ]; then
echo -e "${YELLOW}Warning: Plugin files already exist in target directory.${NC}"
BACKUP_DIR="$TARGET_DIR/mytimeline_backup_$(date +%Y%m%d_%H%M%S)"
echo "Creating backup in: $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$TARGET_DIR/$file" ]; then
cp "$TARGET_DIR/$file" "$BACKUP_DIR/"
fi
done
echo -e "${GREEN}${NC} Backup created"
echo ""
fi
# Copy plugin files
echo "Installing plugin files..."
for file in "${PLUGIN_FILES[@]}"; do
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
echo ""
echo "=========================================="
echo -e "${GREEN}Installation completed successfully!${NC}"
echo "=========================================="
echo ""
echo "Plugin files installed to:"
echo " $TARGET_DIR"
echo ""
echo "Next steps:"
echo " 1. Restart Gramps if it's currently running"
echo " 2. Go to Edit → Preferences → Plugins"
echo " 3. Enable the 'MyTimeline' plugin"
echo " 4. The plugin should appear in the Views menu under 'Families'"
echo ""

165
uninstall_from_snap.sh Executable file
View File

@ -0,0 +1,165 @@
#!/bin/bash
#
# Script to uninstall MyTimeline plugin from snap-installed Gramps
#
# This script removes the MyTimeline plugin files from the Gramps snap plugin directory
#
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
PLUGIN_FILES=("MyTimeline.gpr.py" "MyTimeline.py")
# Target directory for snap-installed Gramps
PLUGIN_DEST_DIR="$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
# Alternative locations to try
ALTERNATIVE_DIRS=(
"$HOME/snap/gramps/current/.local/share/gramps/gramps60/plugins"
"$HOME/snap/gramps/current/.gramps/gramps60/plugins"
"$HOME/snap/gramps/current/.gramps/plugins"
"$HOME/snap/gramps/common/.local/share/gramps/gramps60/plugins"
"$HOME/.local/share/gramps/gramps60/plugins"
"$HOME/.gramps/gramps60/plugins"
)
echo "=========================================="
echo "MyTimeline Plugin Uninstaller for Snap Gramps"
echo "=========================================="
echo ""
# Function to find the correct plugin directory
find_plugin_dir() {
# Try the primary location first
if [ -d "$PLUGIN_DEST_DIR" ]; then
echo "$PLUGIN_DEST_DIR"
return 0
fi
# Try alternative locations
for dir in "${ALTERNATIVE_DIRS[@]}"; do
if [ -d "$dir" ]; then
echo "$dir"
return 0
fi
done
return 1
}
# Find the target directory
echo "Locating Gramps plugin directory..."
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
echo -e "${GREEN}${NC} Target directory: $TARGET_DIR"
echo ""
# Check if plugin files exist
FILES_FOUND=0
FILES_REMOVED=0
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$TARGET_DIR/$file" ]; then
FILES_FOUND=$((FILES_FOUND + 1))
fi
done
if [ $FILES_FOUND -eq 0 ]; then
echo -e "${YELLOW}No plugin files found. The plugin may already be uninstalled.${NC}"
exit 0
fi
echo "Found $FILES_FOUND plugin file(s)."
echo ""
# Remove plugin files
echo "Removing plugin files..."
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$TARGET_DIR/$file" ]; then
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
done
# Remove backup directories created by the installer
BACKUP_DIRS_REMOVED=0
echo ""
echo "Searching for backup directories..."
for backup_dir in "$TARGET_DIR"/mytimeline_backup_*; do
if [ -d "$backup_dir" ]; then
echo "Found backup directory: $(basename "$backup_dir")"
rm -rf "$backup_dir"
echo -e "${GREEN}${NC} Removed backup directory: $(basename "$backup_dir")"
BACKUP_DIRS_REMOVED=$((BACKUP_DIRS_REMOVED + 1))
fi
done
# Also check for any plugin files in subdirectories that might be loaded
echo ""
echo "Checking for plugin files in subdirectories..."
SUBDIR_FILES_REMOVED=0
for subdir in "$TARGET_DIR"/*; do
if [ -d "$subdir" ]; then
for file in "${PLUGIN_FILES[@]}"; do
if [ -f "$subdir/$file" ]; then
echo -e "${YELLOW}Warning: Found $file in subdirectory: $(basename "$subdir")${NC}"
rm -f "$subdir/$file"
echo -e "${GREEN}${NC} Removed: $(basename "$subdir")/$file"
SUBDIR_FILES_REMOVED=$((SUBDIR_FILES_REMOVED + 1))
fi
done
fi
done
TOTAL_REMOVED=$((FILES_REMOVED + BACKUP_DIRS_REMOVED + SUBDIR_FILES_REMOVED))
echo ""
echo "=========================================="
if [ $TOTAL_REMOVED -gt 0 ]; then
echo -e "${GREEN}Uninstallation completed successfully!${NC}"
echo "=========================================="
echo ""
echo "Removed:"
echo " - $FILES_REMOVED plugin file(s) from main directory"
if [ $BACKUP_DIRS_REMOVED -gt 0 ]; then
echo " - $BACKUP_DIRS_REMOVED backup directory/directories"
fi
if [ $SUBDIR_FILES_REMOVED -gt 0 ]; then
echo " - $SUBDIR_FILES_REMOVED plugin file(s) from subdirectories"
fi
echo ""
echo "Location: $TARGET_DIR"
echo ""
echo "Next steps:"
echo " 1. Restart Gramps if it's currently running"
echo " 2. The plugin should no longer appear in the Plugins menu"
else
echo -e "${YELLOW}No files or directories were removed.${NC}"
echo "=========================================="
fi
echo ""