Add portrait display to timeline and tooltips, add portrait generation to demo family

- Add small portraits (24px) next to timeline events for person events
- Add larger portraits (120px) in tooltips when hovering over events
- Implement portrait loading from Gramps media_list using media_path_full
- Add portrait drawing using Cairo with circular clipping and border
- Update demo family generator to create portraits using DiceBear Avatars API
- Generate portraits considering age and gender for appropriate appearance
- Add media objects to XML with proper IDs and gallery references
- Use relative paths for media files in demo family XML
- Add helper scripts for debugging Gramps log files
- Fix deprecation warnings for XML element truth value checks
This commit is contained in:
Daniel Viegas 2025-11-30 11:31:25 +01:00
parent cd58b85b42
commit 7119aedd3d
5 changed files with 631 additions and 21 deletions

View File

@ -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
@ -3317,6 +3426,52 @@ class MyTimelineView(NavigationView):
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]]:
"""
Find the minimum and maximum years from all events.

89
README_LOGGING.md Normal file
View File

@ -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
```

121
check_gramps_logs.sh Executable file
View File

@ -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 ==="

View File

@ -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}")

84
run_gramps_with_logging.sh Executable file
View File

@ -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