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:
parent
cd58b85b42
commit
7119aedd3d
165
MyTimeline.py
165
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
|
||||
@ -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
89
README_LOGGING.md
Normal 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
121
check_gramps_logs.sh
Executable 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 ==="
|
||||
@ -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
84
run_gramps_with_logging.sh
Executable 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
|
||||
Loading…
x
Reference in New Issue
Block a user