templates embedded with project files
This commit is contained in:
parent
9ed8976885
commit
4bf978b75a
@ -109,6 +109,9 @@ class Project:
|
||||
self.export_dpi = 300 # Default export DPI
|
||||
self.page_spacing_mm = 10.0 # Default spacing between pages (1cm)
|
||||
|
||||
# Embedded templates - templates that travel with the project
|
||||
self.embedded_templates: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Initialize asset manager
|
||||
self.asset_manager = AssetManager(self.folder_path)
|
||||
|
||||
@ -195,6 +198,7 @@ class Project:
|
||||
"working_dpi": self.working_dpi,
|
||||
"export_dpi": self.export_dpi,
|
||||
"page_spacing_mm": self.page_spacing_mm,
|
||||
"embedded_templates": self.embedded_templates,
|
||||
"pages": [page.serialize() for page in self.pages],
|
||||
"history": self.history.serialize(),
|
||||
"asset_manager": self.asset_manager.serialize()
|
||||
@ -211,6 +215,10 @@ class Project:
|
||||
self.working_dpi = data.get("working_dpi", 300)
|
||||
self.export_dpi = data.get("export_dpi", 300)
|
||||
self.page_spacing_mm = data.get("page_spacing_mm", 10.0)
|
||||
|
||||
# Deserialize embedded templates
|
||||
self.embedded_templates = data.get("embedded_templates", {})
|
||||
|
||||
self.pages = []
|
||||
|
||||
# Deserialize asset manager
|
||||
|
||||
@ -73,9 +73,10 @@ class Template:
|
||||
class TemplateManager:
|
||||
"""Manager for template operations"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, project=None):
|
||||
self.templates_dir = self._get_templates_directory()
|
||||
self._ensure_templates_directory()
|
||||
self.project = project # Optional project for embedded templates
|
||||
|
||||
def _get_templates_directory(self) -> Path:
|
||||
"""Get the templates directory path"""
|
||||
@ -99,9 +100,14 @@ class TemplateManager:
|
||||
builtin_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def list_templates(self) -> List[str]:
|
||||
"""List all available template names (user + built-in)"""
|
||||
"""List all available template names (embedded + user + built-in)"""
|
||||
templates = []
|
||||
|
||||
# List embedded templates (priority)
|
||||
if self.project and self.project.embedded_templates:
|
||||
for template_name in self.project.embedded_templates.keys():
|
||||
templates.append(f"[Embedded] {template_name}")
|
||||
|
||||
# List user templates
|
||||
if self.templates_dir.exists():
|
||||
for file in self.templates_dir.glob("*.json"):
|
||||
@ -117,12 +123,34 @@ class TemplateManager:
|
||||
return sorted(templates)
|
||||
|
||||
def load_template(self, name: str) -> Template:
|
||||
"""Load a template by name"""
|
||||
"""
|
||||
Load a template by name with priority: embedded > user > built-in.
|
||||
|
||||
Args:
|
||||
name: Template name (may include prefix like '[Embedded]' or '[Built-in]')
|
||||
|
||||
Returns:
|
||||
Template instance
|
||||
"""
|
||||
# Check if it's an embedded template (priority)
|
||||
if name.startswith("[Embedded] "):
|
||||
actual_name = name.replace("[Embedded] ", "")
|
||||
if self.project and actual_name in self.project.embedded_templates:
|
||||
template_data = self.project.embedded_templates[actual_name]
|
||||
return Template.from_dict(template_data)
|
||||
raise FileNotFoundError(f"Embedded template '{actual_name}' not found")
|
||||
|
||||
# Check embedded templates even without prefix (for backward compatibility)
|
||||
if self.project and name in self.project.embedded_templates:
|
||||
template_data = self.project.embedded_templates[name]
|
||||
return Template.from_dict(template_data)
|
||||
|
||||
# Check if it's a built-in template
|
||||
if name.startswith("[Built-in] "):
|
||||
actual_name = name.replace("[Built-in] ", "")
|
||||
template_path = self._get_builtin_templates_directory() / f"{actual_name}.json"
|
||||
else:
|
||||
# User template
|
||||
template_path = self.templates_dir / f"{name}.json"
|
||||
|
||||
if not template_path.exists():
|
||||
@ -130,20 +158,55 @@ class TemplateManager:
|
||||
|
||||
return Template.load_from_file(str(template_path))
|
||||
|
||||
def save_template(self, template: Template):
|
||||
"""Save a template"""
|
||||
template_path = self.templates_dir / f"{template.name}.json"
|
||||
template.save_to_file(str(template_path))
|
||||
def save_template(self, template: Template, embed_in_project: bool = False):
|
||||
"""
|
||||
Save a template to filesystem or embed in project.
|
||||
|
||||
Args:
|
||||
template: Template to save
|
||||
embed_in_project: If True, embed in project instead of saving to filesystem
|
||||
"""
|
||||
if embed_in_project and self.project:
|
||||
# Embed in project
|
||||
self.project.embedded_templates[template.name] = template.to_dict()
|
||||
print(f"Embedded template '{template.name}' in project")
|
||||
else:
|
||||
# Save to filesystem
|
||||
template_path = self.templates_dir / f"{template.name}.json"
|
||||
template.save_to_file(str(template_path))
|
||||
|
||||
def delete_template(self, name: str):
|
||||
"""Delete a template (only user templates)"""
|
||||
"""Delete a template (embedded or user templates only)"""
|
||||
if name.startswith("[Built-in] "):
|
||||
raise PermissionError("Cannot delete built-in templates")
|
||||
|
||||
# Check if it's an embedded template
|
||||
if name.startswith("[Embedded] "):
|
||||
actual_name = name.replace("[Embedded] ", "")
|
||||
if self.project and actual_name in self.project.embedded_templates:
|
||||
del self.project.embedded_templates[actual_name]
|
||||
print(f"Removed embedded template '{actual_name}'")
|
||||
return
|
||||
raise FileNotFoundError(f"Embedded template '{actual_name}' not found")
|
||||
|
||||
# User template from filesystem
|
||||
template_path = self.templates_dir / f"{name}.json"
|
||||
if template_path.exists():
|
||||
template_path.unlink()
|
||||
|
||||
def embed_template(self, template: Template):
|
||||
"""
|
||||
Embed a template in the project.
|
||||
|
||||
Args:
|
||||
template: Template to embed
|
||||
"""
|
||||
if not self.project:
|
||||
raise RuntimeError("No project associated with this TemplateManager")
|
||||
|
||||
self.project.embedded_templates[template.name] = template.to_dict()
|
||||
print(f"Embedded template '{template.name}' in project")
|
||||
|
||||
def create_template_from_page(self, page: Page, name: str, description: str = "") -> Template:
|
||||
"""
|
||||
Create a template from an existing page.
|
||||
@ -297,7 +360,8 @@ class TemplateManager:
|
||||
page: Page,
|
||||
mode: str = "replace",
|
||||
scale_mode: str = "proportional",
|
||||
margin_percent: float = 2.5
|
||||
margin_percent: float = 2.5,
|
||||
auto_embed: bool = True
|
||||
):
|
||||
"""
|
||||
Apply template to an existing page with adjustable margins.
|
||||
@ -309,7 +373,13 @@ class TemplateManager:
|
||||
"reflow" to keep existing content and reposition
|
||||
scale_mode: "proportional", "stretch", or "center"
|
||||
margin_percent: Percentage of page size to use for margins (0-10%)
|
||||
auto_embed: If True, automatically embed template in project
|
||||
"""
|
||||
# Auto-embed template if requested and not already embedded
|
||||
if auto_embed and self.project:
|
||||
if template.name not in self.project.embedded_templates:
|
||||
self.embed_template(template)
|
||||
|
||||
if mode == "replace":
|
||||
# Clear existing elements
|
||||
page.layout.elements.clear()
|
||||
@ -373,7 +443,8 @@ class TemplateManager:
|
||||
template: Template,
|
||||
page_number: int = 1,
|
||||
target_size_mm: Optional[Tuple[float, float]] = None,
|
||||
scale_mode: str = "proportional"
|
||||
scale_mode: str = "proportional",
|
||||
auto_embed: bool = True
|
||||
) -> Page:
|
||||
"""
|
||||
Create a new page from a template.
|
||||
@ -383,10 +454,16 @@ class TemplateManager:
|
||||
page_number: Page number for the new page
|
||||
target_size_mm: Target page size (if different from template)
|
||||
scale_mode: Scaling mode if target_size_mm is provided
|
||||
auto_embed: If True, automatically embed template in project
|
||||
|
||||
Returns:
|
||||
New Page instance with template layout
|
||||
"""
|
||||
# Auto-embed template if requested and not already embedded
|
||||
if auto_embed and self.project:
|
||||
if template.name not in self.project.embedded_templates:
|
||||
self.embed_template(template)
|
||||
|
||||
# Determine page size
|
||||
if target_size_mm is None:
|
||||
page_size = template.page_size_mm
|
||||
|
||||
303
tests/test_embedded_templates.py
Normal file
303
tests/test_embedded_templates.py
Normal file
@ -0,0 +1,303 @@
|
||||
"""
|
||||
Tests for embedded template functionality
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pyPhotoAlbum.project import Project, Page
|
||||
from pyPhotoAlbum.template_manager import TemplateManager, Template
|
||||
from pyPhotoAlbum.models import PlaceholderData, TextBoxData
|
||||
from pyPhotoAlbum.page_layout import PageLayout
|
||||
|
||||
|
||||
def test_embed_template_in_project():
|
||||
"""Test embedding a template in a project"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create a simple template
|
||||
template = Template(name="Test Template", description="A test template")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
|
||||
# Embed the template
|
||||
template_manager.embed_template(template)
|
||||
|
||||
# Verify it's embedded
|
||||
assert "Test Template" in project.embedded_templates
|
||||
assert project.embedded_templates["Test Template"]["name"] == "Test Template"
|
||||
assert len(project.embedded_templates["Test Template"]["elements"]) == 1
|
||||
|
||||
|
||||
def test_load_embedded_template():
|
||||
"""Test loading an embedded template"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create and embed a template
|
||||
template = Template(name="Test Template", description="A test template")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
template_manager.embed_template(template)
|
||||
|
||||
# Load the embedded template
|
||||
loaded_template = template_manager.load_template("Test Template")
|
||||
|
||||
assert loaded_template.name == "Test Template"
|
||||
assert loaded_template.description == "A test template"
|
||||
assert len(loaded_template.elements) == 1
|
||||
|
||||
|
||||
def test_list_embedded_templates():
|
||||
"""Test listing embedded templates alongside filesystem templates"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Embed some templates
|
||||
for i in range(3):
|
||||
template = Template(name=f"Embedded_{i}")
|
||||
template_manager.embed_template(template)
|
||||
|
||||
# List all templates
|
||||
templates = template_manager.list_templates()
|
||||
|
||||
# Check embedded templates are listed with prefix
|
||||
embedded_templates = [t for t in templates if t.startswith("[Embedded]")]
|
||||
assert len(embedded_templates) == 3
|
||||
assert "[Embedded] Embedded_0" in templates
|
||||
assert "[Embedded] Embedded_1" in templates
|
||||
assert "[Embedded] Embedded_2" in templates
|
||||
|
||||
|
||||
def test_embedded_template_priority():
|
||||
"""Test that embedded templates take priority over filesystem templates"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Embed a template with a common name
|
||||
embedded_template = Template(name="Common", description="Embedded version")
|
||||
template_manager.embed_template(embedded_template)
|
||||
|
||||
# Load by name without prefix (should get embedded version)
|
||||
loaded = template_manager.load_template("Common")
|
||||
assert loaded.description == "Embedded version"
|
||||
|
||||
|
||||
def test_serialize_project_with_embedded_templates():
|
||||
"""Test serializing a project with embedded templates"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create and embed a template
|
||||
template = Template(name="Test Template", description="A test template")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
template_manager.embed_template(template)
|
||||
|
||||
# Serialize the project
|
||||
serialized = project.serialize()
|
||||
|
||||
# Verify embedded templates are in serialization
|
||||
assert "embedded_templates" in serialized
|
||||
assert "Test Template" in serialized["embedded_templates"]
|
||||
assert serialized["embedded_templates"]["Test Template"]["name"] == "Test Template"
|
||||
|
||||
|
||||
def test_deserialize_project_with_embedded_templates():
|
||||
"""Test deserializing a project with embedded templates"""
|
||||
# Create a project with embedded template
|
||||
project = Project(name="Test Project")
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
template = Template(name="Test Template", description="A test template")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
template_manager.embed_template(template)
|
||||
|
||||
# Serialize the project
|
||||
serialized = project.serialize()
|
||||
|
||||
# Create a new project and deserialize
|
||||
new_project = Project(name="New Project")
|
||||
new_project.deserialize(serialized)
|
||||
|
||||
# Verify embedded templates were restored
|
||||
assert "Test Template" in new_project.embedded_templates
|
||||
assert new_project.embedded_templates["Test Template"]["name"] == "Test Template"
|
||||
|
||||
# Verify we can load the template from the new project
|
||||
new_template_manager = TemplateManager(project=new_project)
|
||||
loaded_template = new_template_manager.load_template("Test Template")
|
||||
assert loaded_template.name == "Test Template"
|
||||
assert len(loaded_template.elements) == 1
|
||||
|
||||
|
||||
def test_auto_embed_on_apply():
|
||||
"""Test that templates are automatically embedded when applied"""
|
||||
# Create a project and page
|
||||
project = Project(name="Test Project")
|
||||
page = Page()
|
||||
project.add_page(page)
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create a template (not embedded yet)
|
||||
template = Template(name="Auto Embed Test", description="Should auto-embed")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
|
||||
# Apply template with auto_embed=True (default)
|
||||
template_manager.apply_template_to_page(template, page)
|
||||
|
||||
# Verify template was auto-embedded
|
||||
assert "Auto Embed Test" in project.embedded_templates
|
||||
|
||||
|
||||
def test_auto_embed_on_create_page():
|
||||
"""Test that templates are automatically embedded when creating pages"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create a template (not embedded yet)
|
||||
template = Template(name="Auto Embed Page Test", description="Should auto-embed")
|
||||
placeholder = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=100, height=100
|
||||
)
|
||||
template.add_element(placeholder)
|
||||
|
||||
# Create page from template with auto_embed=True (default)
|
||||
page = template_manager.create_page_from_template(template, page_number=1)
|
||||
|
||||
# Verify template was auto-embedded
|
||||
assert "Auto Embed Page Test" in project.embedded_templates
|
||||
|
||||
|
||||
def test_delete_embedded_template():
|
||||
"""Test deleting an embedded template"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Embed a template
|
||||
template = Template(name="To Delete")
|
||||
template_manager.embed_template(template)
|
||||
|
||||
assert "To Delete" in project.embedded_templates
|
||||
|
||||
# Delete the embedded template
|
||||
template_manager.delete_template("[Embedded] To Delete")
|
||||
|
||||
assert "To Delete" not in project.embedded_templates
|
||||
|
||||
|
||||
def test_embedded_template_with_text():
|
||||
"""Test embedding template with text elements"""
|
||||
# Create a project
|
||||
project = Project(name="Test Project")
|
||||
|
||||
# Create a template manager with the project
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create a template with text
|
||||
template = Template(name="Text Template")
|
||||
textbox = TextBoxData(
|
||||
text_content="Sample Text",
|
||||
x=10, y=10, width=200, height=50
|
||||
)
|
||||
template.add_element(textbox)
|
||||
|
||||
# Embed and reload
|
||||
template_manager.embed_template(template)
|
||||
loaded = template_manager.load_template("Text Template")
|
||||
|
||||
assert len(loaded.elements) == 1
|
||||
assert isinstance(loaded.elements[0], TextBoxData)
|
||||
assert loaded.elements[0].text_content == "Sample Text"
|
||||
|
||||
|
||||
def test_roundtrip_serialization():
|
||||
"""Test complete roundtrip: create project, embed template, serialize, deserialize"""
|
||||
# Create a project with pages and embedded template
|
||||
project = Project(name="Roundtrip Test")
|
||||
template_manager = TemplateManager(project=project)
|
||||
|
||||
# Create a template
|
||||
template = Template(name="Roundtrip Template", page_size_mm=(200, 300))
|
||||
placeholder1 = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=10, y=10, width=80, height=80
|
||||
)
|
||||
placeholder2 = PlaceholderData(
|
||||
placeholder_type="image",
|
||||
x=110, y=10, width=80, height=80
|
||||
)
|
||||
template.add_element(placeholder1)
|
||||
template.add_element(placeholder2)
|
||||
|
||||
# Create a page from this template
|
||||
page = template_manager.create_page_from_template(template, page_number=1)
|
||||
project.add_page(page)
|
||||
|
||||
# Serialize
|
||||
serialized = project.serialize()
|
||||
|
||||
# Create new project and deserialize
|
||||
new_project = Project(name="New Roundtrip")
|
||||
new_project.deserialize(serialized)
|
||||
|
||||
# Verify embedded template
|
||||
assert "Roundtrip Template" in new_project.embedded_templates
|
||||
|
||||
# Verify we can use the template
|
||||
new_template_manager = TemplateManager(project=new_project)
|
||||
loaded_template = new_template_manager.load_template("Roundtrip Template")
|
||||
|
||||
assert loaded_template.name == "Roundtrip Template"
|
||||
assert loaded_template.page_size_mm == (200, 300)
|
||||
assert len(loaded_template.elements) == 2
|
||||
|
||||
# Create another page from the loaded template
|
||||
new_page = new_template_manager.create_page_from_template(
|
||||
loaded_template,
|
||||
page_number=2,
|
||||
auto_embed=False # Don't embed again
|
||||
)
|
||||
assert len(new_page.layout.elements) == 2
|
||||
Loading…
x
Reference in New Issue
Block a user