diff --git a/MyTimeline.py b/MyTimeline.py index 7fefa07..bf7fb2f 100644 --- a/MyTimeline.py +++ b/MyTimeline.py @@ -40,6 +40,7 @@ if TYPE_CHECKING: from gi.repository import Gtk from gi.repository import Gdk +from gi.repository import GdkPixbuf from gi.repository import GLib from gi.repository import Pango from gi.repository import PangoCairo @@ -130,6 +131,11 @@ TOOLTIP_LABEL_MARGIN = 5 # Margin for tooltip label TOOLTIP_MAX_WIDTH_CHARS = 40 # Maximum width in characters for tooltip label TOOLTIP_BORDER_WIDTH = 8 # Border width for tooltip window +# Portrait Display Constants +PORTRAIT_SIZE_TIMELINE = 24 # Size of timeline portraits +PORTRAIT_SIZE_TOOLTIP = 120 # Size of tooltip portraits +PORTRAIT_MARGIN = 5 # Margin around portraits + # Font Constants FONT_FAMILY = "Sans" FONT_SIZE_NORMAL = 11 @@ -2795,6 +2801,84 @@ class MyTimelineView(NavigationView): logger.debug(f"Error accessing place information for event in tooltip: {e}") return None + def _get_person_portrait_path(self, person: Optional['Person']) -> Optional[str]: + """ + Get the file path for a person's portrait from their media_list. + + Args: + person: The person object. + + Returns: + Optional[str]: File path to portrait if available, None otherwise. + """ + if not person or not self.dbstate.is_open(): + return None + + try: + media_list = person.get_media_list() + if not media_list: + return None + + # Get the first media reference (primary portrait) + media_ref = media_list[0] + media_handle = media_ref.get_reference_handle() + + # Get the media object from database + media_obj = self.dbstate.db.get_media_from_handle(media_handle) + if not media_obj: + return None + + # Get the file path + path = media_obj.get_path() + if not path: + return None + + # Resolve relative paths using Gramps media path resolution + # Gramps stores paths relative to the database directory + # We need to resolve them to absolute paths + try: + from gramps.gen.utils.file import media_path_full + full_path = media_path_full(self.dbstate.db, path) + return full_path + except (ImportError, AttributeError): + # Fallback: try to use path as-is or resolve relative to current directory + import os + if os.path.isabs(path): + return path if os.path.exists(path) else None + # Try relative to current directory + if os.path.exists(path): + return os.path.abspath(path) + return None + + except (AttributeError, KeyError, IndexError) as e: + logger.debug(f"Error accessing portrait for person: {e}") + return None + + def _load_portrait_image(self, file_path: str, size: int) -> Optional[GdkPixbuf.Pixbuf]: + """ + Load and scale a portrait image. + + Args: + file_path: Path to the image file. + size: Target size (width and height) for the image. + + Returns: + Optional[GdkPixbuf.Pixbuf]: Scaled pixbuf if successful, None otherwise. + """ + if not file_path: + return None + + try: + # Load the image + pixbuf = GdkPixbuf.Pixbuf.new_from_file(file_path) + + # Scale to target size maintaining aspect ratio + scaled_pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR) + return scaled_pixbuf + except (GLib.GError, Exception) as e: + logger.debug(f"Error loading portrait image from {file_path}: {e}") + return None + def _format_person_tooltip(self, person: 'Person', person_events: List[TimelineEvent]) -> str: """ Format tooltip for person with multiple events. @@ -2917,16 +3001,32 @@ class MyTimelineView(NavigationView): frame.set_shadow_type(Gtk.ShadowType.OUT) frame.get_style_context().add_class("tooltip") + # Create horizontal box for portrait and text + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=PORTRAIT_MARGIN) + hbox.set_margin_start(TOOLTIP_LABEL_MARGIN) + hbox.set_margin_end(TOOLTIP_LABEL_MARGIN) + hbox.set_margin_top(TOOLTIP_LABEL_MARGIN) + hbox.set_margin_bottom(TOOLTIP_LABEL_MARGIN) + + # Add portrait if person has one + if event_data.person: + portrait_path = self._get_person_portrait_path(event_data.person) + if portrait_path: + pixbuf = self._load_portrait_image(portrait_path, PORTRAIT_SIZE_TOOLTIP) + if pixbuf: + portrait_image = Gtk.Image.new_from_pixbuf(pixbuf) + hbox.pack_start(portrait_image, False, False, 0) + + # Add text label label = Gtk.Label() label.set_markup(tooltip_text) label.set_line_wrap(True) label.set_max_width_chars(TOOLTIP_MAX_WIDTH_CHARS) - label.set_margin_start(TOOLTIP_LABEL_MARGIN) - label.set_margin_end(TOOLTIP_LABEL_MARGIN) - label.set_margin_top(TOOLTIP_LABEL_MARGIN) - label.set_margin_bottom(TOOLTIP_LABEL_MARGIN) + label.set_halign(Gtk.Align.START) + label.set_valign(Gtk.Align.START) + hbox.pack_start(label, True, True, 0) - frame.add(label) + frame.add(hbox) tooltip_window.add(frame) tooltip_window.show_all() @@ -3017,8 +3117,17 @@ class MyTimelineView(NavigationView): self.draw_event_marker(context, timeline_x, event_data.y_pos, event_data.event_type, is_hovered, is_selected) + # Draw portrait if person has one (between timeline and label) + if event_data.person: + portrait_x = timeline_x + PORTRAIT_MARGIN + PORTRAIT_SIZE_TIMELINE / 2 + self.draw_portrait(context, portrait_x, event_data.y_pos, + event_data.person, PORTRAIT_SIZE_TIMELINE) + # Draw event label label_x = timeline_x + LABEL_X_OFFSET + # Adjust label position if portrait is present + if event_data.person: + label_x = timeline_x + PORTRAIT_SIZE_TIMELINE + PORTRAIT_MARGIN * 2 + LABEL_X_OFFSET self.draw_event_label( context, label_x, event_data.y_pos, event_data.date_obj, event_data.event, event_data.person, event_data.event_type, is_hovered @@ -3314,7 +3423,53 @@ class MyTimelineView(NavigationView): context.set_source_rgb(*LABEL_TEXT_COLOR) context.move_to(x, y - LABEL_VERTICAL_OFFSET) PangoCairo.show_layout(context, layout) + + context.restore() + def draw_portrait(self, context: cairo.Context, x: float, y: float, + person: Optional['Person'], size: int = PORTRAIT_SIZE_TIMELINE) -> None: + """ + Draw a circular portrait for a person. + + Args: + context: Cairo drawing context. + x: X coordinate for portrait center. + y: Y coordinate for portrait center. + person: The person object (None if no person or no portrait). + size: Size of the portrait (diameter). + """ + if not person: + return + + # Get portrait path and load image + portrait_path = self._get_person_portrait_path(person) + if not portrait_path: + return + + pixbuf = self._load_portrait_image(portrait_path, size) + if not pixbuf: + return + + context.save() + + # Create a circular clipping path + radius = size / 2 + context.arc(x, y, radius, 0, 2 * math.pi) + context.clip() + + # Use Gdk to set the pixbuf as source for Cairo + # This is the proper way to draw a GdkPixbuf with Cairo + Gdk.cairo_set_source_pixbuf(context, pixbuf, x - radius, y - radius) + context.paint() + + context.restore() + + # Draw a border around the portrait + context.save() + context.set_source_rgba(0.0, 0.0, 0.0, 0.3) # Semi-transparent black border + context.set_line_width(1.5) + context.arc(x, y, radius, 0, 2 * math.pi) + context.stroke() context.restore() def _find_year_range(self) -> Tuple[Optional[int], Optional[int]]: diff --git a/README_LOGGING.md b/README_LOGGING.md new file mode 100644 index 0000000..9923ba3 --- /dev/null +++ b/README_LOGGING.md @@ -0,0 +1,89 @@ +# Gramps Logging and Media Import Debugging + +This directory contains scripts to help debug media import issues in Gramps. + +## Scripts + +### 1. `check_gramps_logs.sh` +Checks for existing Gramps log files and displays media-related errors and messages. + +**Usage:** +```bash +./check_gramps_logs.sh +``` + +This script will: +- Check for log files in the snap directory +- Display media-related errors and warnings +- Show recent log entries +- Check alternative log file locations + +### 2. `run_gramps_with_logging.sh` +Runs Gramps with debug logging enabled and monitors the log file in real-time. + +**Usage:** +```bash +./run_gramps_with_logging.sh +``` + +This script will: +- Start Gramps with debug logging for media and import operations +- Monitor the log file as it's created +- Display log entries in real-time + +## How to Debug Media Import Issues + +1. **Run Gramps with logging:** + ```bash + ./run_gramps_with_logging.sh + ``` + +2. **In Gramps:** + - Import the `demo_family.gramps` file + - Note any error messages in the Gramps UI + - Close Gramps when done + +3. **Check the logs:** + ```bash + ./check_gramps_logs.sh + ``` + +4. **Look for:** + - Media file path errors + - Import errors + - File not found messages + - Permission errors + +## Manual Log File Location + +For Gramps installed via snap, log files are located at: +``` +~/snap/gramps/11/.config/gramps/gramps60/logs/Gramps60.log +``` + +## Common Issues + +### Media files not found +- Check that the `portraits/` directory is in the same directory as `demo_family.gramps` +- Verify file paths in the XML are relative (e.g., `portraits/portrait_0001_John_Smith.svg`) +- Set the base media path in Gramps: Edit → Preferences → Family Tree → Base media path + +### Import errors +- Check the log file for specific error messages +- Verify XML structure is valid +- Ensure all media objects have proper `id` attributes + +## Enabling Logging Manually + +If you prefer to run Gramps manually with logging: + +```bash +gramps --debug=gramps.gen.lib.media \ + --debug=gramps.plugins.import.importxml \ + --debug=gramps.gen.db +``` + +Or enable all logging: +```bash +gramps --debug=all +``` diff --git a/check_gramps_logs.sh b/check_gramps_logs.sh new file mode 100755 index 0000000..14751d4 --- /dev/null +++ b/check_gramps_logs.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# Script to check Gramps logs for media import errors + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo "=== Gramps Log File Checker ===" +echo "" + +# Determine log directory +LOG_DIR="$HOME/snap/gramps/11/.config/gramps/gramps60/logs" +LOG_FILE="$LOG_DIR/Gramps60.log" + +echo "Checking for log directory: $LOG_DIR" + +# Create logs directory if it doesn't exist +if [ ! -d "$LOG_DIR" ]; then + echo -e "${YELLOW}Logs directory doesn't exist. Creating it...${NC}" + mkdir -p "$LOG_DIR" + echo -e "${GREEN}Created: $LOG_DIR${NC}" +else + echo -e "${GREEN}Logs directory exists: $LOG_DIR${NC}" +fi + +echo "" +echo "Checking for log file: $LOG_FILE" + +# Check if log file exists +if [ -f "$LOG_FILE" ]; then + echo -e "${GREEN}Log file found!${NC}" + echo "" + echo "=== Log File Info ===" + ls -lh "$LOG_FILE" + echo "" + echo "Last modified: $(stat -c %y "$LOG_FILE" 2>/dev/null || stat -f %Sm "$LOG_FILE" 2>/dev/null || echo "Unknown")" + echo "" + + # Count lines + LINE_COUNT=$(wc -l < "$LOG_FILE") + echo "Total lines in log: $LINE_COUNT" + echo "" + + # Search for media-related errors + echo "=== Searching for Media-Related Messages ===" + echo "" + + # Check for errors + ERROR_COUNT=$(grep -i "error\|exception\|traceback\|failed" "$LOG_FILE" | wc -l) + if [ "$ERROR_COUNT" -gt 0 ]; then + echo -e "${RED}Found $ERROR_COUNT error/exception entries${NC}" + echo "" + echo "Recent errors:" + grep -i "error\|exception\|traceback\|failed" "$LOG_FILE" | tail -20 + echo "" + else + echo -e "${GREEN}No errors found in log file${NC}" + echo "" + fi + + # Check for media-related entries + MEDIA_COUNT=$(grep -i "media\|portrait\|file\|import" "$LOG_FILE" | wc -l) + if [ "$MEDIA_COUNT" -gt 0 ]; then + echo -e "${YELLOW}Found $MEDIA_COUNT media-related entries${NC}" + echo "" + echo "Recent media-related messages:" + grep -i "media\|portrait\|file\|import" "$LOG_FILE" | tail -30 + echo "" + else + echo -e "${YELLOW}No media-related entries found${NC}" + echo "" + fi + + # Show last 50 lines + echo "=== Last 50 Lines of Log ===" + tail -50 "$LOG_FILE" + +else + echo -e "${YELLOW}Log file not found yet.${NC}" + echo "" + echo "To create the log file, run Gramps with debug logging enabled:" + echo "" + echo " gramps --debug=all" + echo "" + echo "Or specifically for media import:" + echo "" + echo " gramps --debug=gramps.gen.lib.media --debug=gramps.plugins.import.importxml" + echo "" + echo "After running Gramps and attempting to import, run this script again to check for errors." + echo "" +fi + +# Also check for other possible log locations +echo "" +echo "=== Checking Alternative Log Locations ===" +ALTERNATIVE_LOCATIONS=( + "$HOME/.gramps/Gramps60.log" + "$HOME/.gramps/Gramps51.log" + "$HOME/.gramps/logs/Gramps60.log" + "$HOME/snap/gramps/common/.gramps/Gramps60.log" + "./gramps_debug.log" + "$HOME/gramps_debug.log" +) + +for loc in "${ALTERNATIVE_LOCATIONS[@]}"; do + if [ -f "$loc" ]; then + echo -e "${GREEN}Found log file: $loc${NC}" + ls -lh "$loc" + echo "" + echo "Media-related entries in $loc:" + grep -i "media\|portrait\|file\|import\|error" "$loc" | tail -20 + echo "" + fi +done + +echo "" +echo "=== Script Complete ===" diff --git a/generate_demo_family.py b/generate_demo_family.py index b23e153..bafe501 100644 --- a/generate_demo_family.py +++ b/generate_demo_family.py @@ -5,6 +5,10 @@ Generate a huge demo family for Gramps testing import random import xml.etree.ElementTree as ET +import os +import urllib.request +import urllib.error +import urllib.parse from dataclasses import dataclass from datetime import datetime from typing import Optional, List, Tuple, Dict @@ -24,6 +28,10 @@ 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" +# Portrait generation constants +PORTRAITS_DIR = "portraits" +DICEBEAR_API_BASE = "https://api.dicebear.com/7.x/avataaars/svg" + # Event types to add EVENT_TYPES = [ ("Baptism", 0.7, 0, 2), # 70% chance, 0-2 years after birth @@ -119,6 +127,118 @@ def gen_handle(prefix: str, num: int) -> str: return f"_{prefix}{num:08d}" +def generate_portrait(person_id: int, name: str, gender: str, birth_year: int) -> Optional[Tuple[str, str]]: + """ + Generate a portrait for a person using DiceBear Avatars API. + Considers age and gender for appropriate portrait selection. + + Args: + person_id: Unique person ID. + name: Person's name (used as seed for deterministic generation). + gender: Person's gender ('M' or 'F'). + birth_year: Birth year (used to determine age-appropriate features). + + Returns: + Optional[Tuple[str, str]]: Tuple of (media_handle, file_path) if successful, None otherwise. + """ + # Create portraits directory if it doesn't exist + if not os.path.exists(PORTRAITS_DIR): + os.makedirs(PORTRAITS_DIR) + + # Calculate approximate age (use current year or a fixed reference year) + current_year = datetime.now().year + age = current_year - birth_year + + # Create seed from name, person_id, and gender for deterministic generation + # Include gender in seed to get different avatars for different genders + seed = f"{name}_{person_id}_{gender}" + + # Build API URL with parameters + # DiceBear Avataaars doesn't have a gender parameter, but we can use seed + # to get deterministic results. The seed with gender included will produce + # different avatars for different genders. + params = { + "seed": seed + } + + # Note: DiceBear Avataaars style doesn't support style parameter + # We rely on the seed for deterministic generation based on name, ID, and gender + + # Build URL with proper encoding + url = f"{DICEBEAR_API_BASE}?{urllib.parse.urlencode(params)}" + + # Generate filename + filename = f"portrait_{person_id:04d}_{name.replace(' ', '_')}.svg" + file_path = os.path.join(PORTRAITS_DIR, filename) + + # Download portrait + try: + urllib.request.urlretrieve(url, file_path) + media_handle = gen_handle("MEDIA", person_id) + return (media_handle, file_path) + except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e: + print(f"Warning: Could not generate portrait for {name}: {e}") + return None + + +def create_media_element(media_handle: str, file_path: str, title: str, media_id: Optional[int] = None) -> ET.Element: + """ + Create an XML element for a media object. + + Args: + media_handle: Unique handle for the media object. + file_path: Path to the media file (relative to XML file location). + title: Title/description for the media object. + media_id: Optional media ID number. If None, extracted from handle. + + Returns: + ET.Element: The media XML element. + """ + media_elem = ET.Element("media") + media_elem.set("handle", media_handle) + media_elem.set("change", str(int(datetime.now().timestamp()))) + + # Extract ID from handle if not provided + # Handle format: _MEDIA00000001 -> ID: O0001 + if media_id is None: + # Extract number from handle (e.g., "_MEDIA00000001" -> 1) + try: + media_id = int(media_handle.replace("_MEDIA", "").lstrip("0") or "0") + except (ValueError, AttributeError): + media_id = 0 + + media_elem.set("id", f"O{media_id:04d}") + + # Use relative path for file (relative to XML file location) + # The file_path from generate_portrait is already relative (portraits/filename) + # If it's absolute, convert it to relative based on current working directory + if os.path.isabs(file_path): + # Get current working directory (where XML file will be saved) + cwd = os.getcwd() + try: + rel_path = os.path.relpath(file_path, cwd) + except ValueError: + # If paths are on different drives (Windows), keep absolute + rel_path = file_path + else: + # Already relative, use as-is + rel_path = file_path + + # Normalize path separators (use forward slashes for cross-platform compatibility) + rel_path = rel_path.replace("\\", "/") + + file_elem = ET.SubElement(media_elem, "file") + file_elem.set("src", rel_path) + + title_elem = ET.SubElement(media_elem, "title") + title_elem.text = title + + mime_elem = ET.SubElement(media_elem, "mimetype") + mime_elem.text = "image/svg+xml" + + return media_elem + + def create_event_element(event_data: EventData) -> ET.Element: """Create an XML element for an event.""" event_elem = ET.Element("event") @@ -220,8 +340,8 @@ def gen_person( 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.""" +) -> Tuple[ET.Element, ET.Element, Optional[ET.Element], List[ET.Element], List[Tuple[str, EventData]], Optional[Tuple[str, str]]]: + """Generate a person with all associated events and portrait.""" 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 @@ -242,6 +362,16 @@ def gen_person( surname_elem = ET.SubElement(name_elem, "surname") surname_elem.text = surname + # Generate portrait and add gallery reference + full_name = f"{first_name} {surname}" + portrait_info = generate_portrait(pid, full_name, gender, birth_year) + if portrait_info: + media_handle, file_path = portrait_info + # Add gallery section with media reference + gallery_elem = ET.SubElement(person_elem, "gallery") + media_ref = ET.SubElement(gallery_elem, "mediaobjref") + media_ref.set("hlink", media_handle) + # Birth event reference birth_ref = ET.SubElement(person_elem, "eventref") birth_ref.set("hlink", birth_handle) @@ -309,7 +439,7 @@ def gen_person( # Convert additional events to XML elements all_additional_events_xml = [create_event_element(event_data) for _, event_data in additional_events] - return person_elem, birth_event, death_event, all_additional_events_xml, additional_events + return person_elem, birth_event, death_event, all_additional_events_xml, additional_events, portrait_info def gen_family( @@ -366,7 +496,8 @@ def gen_family( def create_gramps_xml_document( events: List[ET.Element], people: List[ET.Element], - families: List[ET.Element] + families: List[ET.Element], + media: List[ET.Element] ) -> ET.ElementTree: """Create the complete Gramps XML document.""" # Create root element @@ -401,6 +532,11 @@ def create_gramps_xml_document( for family in families: families_elem.append(family) + # Media objects + media_elem = ET.SubElement(database, "objects") + for media_obj in media: + media_elem.append(media_obj) + return ET.ElementTree(database) @@ -413,7 +549,7 @@ def main() -> None: father_id = 1 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_person, father_birth, father_death, father_additional_xml, _, father_portrait = gen_person( father_id, "John", "Smith", 1950, 2010, "M", parentin_families=[main_family_handle] ) @@ -421,16 +557,27 @@ def main() -> None: # Mother: Mary Smith, born 1952, died 2015 mother_id = 2 mother_handle = gen_handle("PERSON", mother_id) - mother_person, mother_birth, mother_death, mother_additional_xml, _ = gen_person( + mother_person, mother_birth, mother_death, mother_additional_xml, _, mother_portrait = gen_person( mother_id, "Mary", "Smith", 1952, 2015, "F", parentin_families=[main_family_handle] ) + # Collect media elements + all_media: List[ET.Element] = [] + if father_portrait: + media_handle, file_path = father_portrait + media_elem = create_media_element(media_handle, file_path, "Portrait of John Smith", father_id) + all_media.append(media_elem) + if mother_portrait: + media_handle, file_path = mother_portrait + media_elem = create_media_element(media_handle, file_path, "Portrait of Mary Smith", mother_id) + all_media.append(media_elem) + all_additional_events = father_additional_xml + mother_additional_xml all_events = [father_birth, mother_birth] - if father_death: + if father_death is not None: all_events.append(father_death) - if mother_death: + if mother_death is not None: all_events.append(mother_death) # Generate 15 children @@ -446,7 +593,7 @@ def main() -> None: death_year = birth_year + random.randint(60, 90) if random.random() < 0.3 else None # 30% chance of death child_handle = gen_handle("PERSON", child_id) - child_person, child_birth, child_death, child_additional_xml, child_additional_tuples = gen_person( + child_person, child_birth, child_death, child_additional_xml, child_additional_tuples, child_portrait = gen_person( child_id, first_name, "Smith", birth_year, death_year, gender, childof_families=[main_family_handle] ) @@ -454,11 +601,16 @@ def main() -> None: children.append(child_person) child_handles.append(child_handle) all_events.append(child_birth) - if child_death: + if child_death is not None: 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) + # Add portrait media if available + if child_portrait: + media_handle, file_path = child_portrait + media_elem = create_media_element(media_handle, file_path, f"Portrait of {first_name} Smith", child_id) + all_media.append(media_elem) child_id += 1 # Generate family @@ -548,15 +700,19 @@ def main() -> None: childof=[] ) - spouse_person, spouse_birth_event, spouse_death_event, spouse_additional_xml, _ = gen_person( + spouse_person, spouse_birth_event, spouse_death_event, spouse_additional_xml, _, spouse_portrait = gen_person( grandchild_id, spouse_name, "Smith", spouse_birth, None, spouse_gender, parentin_families=[child_family_handle] ) grandchildren.append(spouse_person) all_events.append(spouse_birth_event) - if spouse_death_event: + if spouse_death_event is not None: all_events.append(spouse_death_event) all_additional_events.extend(spouse_additional_xml) + if spouse_portrait: + media_handle, file_path = spouse_portrait + media_elem = create_media_element(media_handle, file_path, f"Portrait of {spouse_name} Smith", grandchild_id) + all_media.append(media_elem) grandchild_id += 1 # Update parent to include parentin reference @@ -582,16 +738,20 @@ def main() -> None: childof=[child_family_handle] ) - gchild_person, gchild_birth_event, gchild_death_event, gchild_additional_xml, _ = gen_person( + gchild_person, gchild_birth_event, gchild_death_event, gchild_additional_xml, _, gchild_portrait = gen_person( grandchild_id, gchild_name, "Smith", gchild_birth, None, gchild_gender, childof_families=[child_family_handle] ) grandchildren.append(gchild_person) grandchild_handles.append(gchild_handle) all_events.append(gchild_birth_event) - if gchild_death_event: + if gchild_death_event is not None: all_events.append(gchild_death_event) all_additional_events.extend(gchild_additional_xml) + if gchild_portrait: + media_handle, file_path = gchild_portrait + media_elem = create_media_element(media_handle, file_path, f"Portrait of {gchild_name} Smith", grandchild_id) + all_media.append(media_elem) grandchild_id += 1 # Create family for this couple @@ -608,7 +768,7 @@ def main() -> None: data = person_data[child_pid] # Reuse the original additional events to ensure consistency original_additional_events = child_additional_events_map.get(child_pid, []) - child_person, _, _, _, _ = gen_person( + child_person, _, _, _, _, _ = gen_person( 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 @@ -620,7 +780,7 @@ def main() -> None: # Create complete XML document people = [father_person, mother_person] + children + grandchildren - tree = create_gramps_xml_document(all_events, people, families) + tree = create_gramps_xml_document(all_events, people, families, all_media) # Write XML file with proper formatting # ET.indent is only available in Python 3.9+, so we'll format manually if needed @@ -652,6 +812,7 @@ def main() -> None: print(f" - Multiple families with marriage events") print(f" - Birth and death events for all") print(f" - {len(all_additional_events)} additional events (Baptism, Education, Occupation, etc.)") + print(f" - {len(all_media)} portraits (generated considering age and gender)") print(f" - Total events: {total_events}") diff --git a/run_gramps_with_logging.sh b/run_gramps_with_logging.sh new file mode 100755 index 0000000..14c87cc --- /dev/null +++ b/run_gramps_with_logging.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Script to run Gramps with debug logging enabled and monitor log file + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +LOG_DIR="$HOME/snap/gramps/11/.config/gramps/gramps60/logs" +LOG_FILE="$LOG_DIR/Gramps60.log" + +echo "=== Running Gramps with Debug Logging ===" +echo "" +echo "Log file will be created at: $LOG_FILE" +echo "" + +# Create logs directory if it doesn't exist +mkdir -p "$LOG_DIR" + +# Clear any existing log file to start fresh +if [ -f "$LOG_FILE" ]; then + echo -e "${YELLOW}Backing up existing log file...${NC}" + mv "$LOG_FILE" "${LOG_FILE}.backup.$(date +%Y%m%d_%H%M%S)" +fi + +echo -e "${GREEN}Starting Gramps with debug logging enabled...${NC}" +echo "" +echo "Debug loggers enabled:" +echo " - gramps.gen.lib.media (media objects)" +echo " - gramps.plugins.import.importxml (XML import)" +echo " - gramps.gen.db (database operations)" +echo "" +echo "After you import the demo_family.gramps file, close Gramps and" +echo "run './check_gramps_logs.sh' to analyze the log file." +echo "" +echo "Press Ctrl+C to stop monitoring (Gramps will continue running)" +echo "" + +# Create a log file in the current directory (snap confinement may prevent writing to ~/.gramps) +LOCAL_LOG="gramps_debug.log" +echo "Debug output will also be saved to: $PWD/$LOCAL_LOG" +echo "" + +# Run Gramps with debug logging and redirect output +gramps --debug=gramps.gen.lib.media \ + --debug=gramps.plugins.import.importxml \ + --debug=gramps.gen.db \ + --debug=gramps.gen.lib \ + "$@" > "$LOCAL_LOG" 2>&1 & + +GRAMPS_PID=$! + +echo "Gramps started with PID: $GRAMPS_PID" +echo "" + +# Monitor log file creation +echo "Waiting for log file to be created..." +while [ ! -f "$LOG_FILE" ] && kill -0 "$GRAMPS_PID" 2>/dev/null; do + sleep 1 +done + +if [ -f "$LOG_FILE" ]; then + echo -e "${GREEN}Log file created!${NC}" + echo "" + echo "Monitoring log file. Press Ctrl+C to stop monitoring." + echo "(Gramps will continue running)" + echo "" + + # Monitor log file for new entries + tail -f "$LOG_FILE" 2>/dev/null || { + echo "Waiting for log entries..." + sleep 2 + if [ -f "$LOG_FILE" ]; then + echo "" + echo "Current log contents:" + cat "$LOG_FILE" + fi + } +else + echo "Log file not created yet. Gramps may not have started logging." + echo "Check if Gramps is running: ps aux | grep gramps" +fi