203 lines
6.6 KiB
Python
203 lines
6.6 KiB
Python
"""
|
|
Template service for applying HTML/CSS templates to generated content
|
|
"""
|
|
|
|
import json
|
|
import random
|
|
from pathlib import Path
|
|
from typing import Optional, Dict
|
|
from src.core.config import get_config
|
|
from src.database.repositories import GeneratedContentRepository
|
|
|
|
|
|
class TemplateService:
|
|
"""Service for loading, selecting, and applying HTML templates"""
|
|
|
|
def __init__(self, content_repo: Optional[GeneratedContentRepository] = None):
|
|
self.templates_dir = Path(__file__).parent / "templates"
|
|
self._template_cache: Dict[str, str] = {}
|
|
self.content_repo = content_repo
|
|
|
|
def get_available_templates(self) -> list[str]:
|
|
"""
|
|
Get list of available template names
|
|
|
|
Returns:
|
|
List of template names without .html extension
|
|
"""
|
|
templates = []
|
|
for template_file in self.templates_dir.glob("*.html"):
|
|
templates.append(template_file.stem)
|
|
return sorted(templates)
|
|
|
|
def load_template(self, template_name: str) -> str:
|
|
"""
|
|
Load a template file by name
|
|
|
|
Args:
|
|
template_name: Name of template (without .html extension)
|
|
|
|
Returns:
|
|
Template content as string
|
|
|
|
Raises:
|
|
FileNotFoundError: If template doesn't exist
|
|
"""
|
|
if template_name in self._template_cache:
|
|
return self._template_cache[template_name]
|
|
|
|
template_path = self.templates_dir / f"{template_name}.html"
|
|
|
|
if not template_path.exists():
|
|
available = self.get_available_templates()
|
|
raise FileNotFoundError(
|
|
f"Template '{template_name}' not found. Available templates: {', '.join(available)}"
|
|
)
|
|
|
|
with open(template_path, 'r', encoding='utf-8') as f:
|
|
template_content = f.read()
|
|
|
|
self._template_cache[template_name] = template_content
|
|
return template_content
|
|
|
|
def select_template_for_content(
|
|
self,
|
|
site_deployment_id: Optional[int] = None,
|
|
site_deployment_repo=None
|
|
) -> str:
|
|
"""
|
|
Select appropriate template based on site deployment
|
|
|
|
Args:
|
|
site_deployment_id: Optional site deployment ID
|
|
site_deployment_repo: Optional repository for querying site deployments
|
|
|
|
Returns:
|
|
Template name to use
|
|
|
|
Logic:
|
|
1. If site_deployment_id exists:
|
|
- Query custom_hostname from SiteDeployment
|
|
- Check config mappings for hostname
|
|
- If mapping exists, use it
|
|
- If no mapping, randomly select and persist to config
|
|
2. If site_deployment_id is null: randomly select (don't persist)
|
|
"""
|
|
config = get_config()
|
|
|
|
if site_deployment_id and site_deployment_repo:
|
|
site_deployment = site_deployment_repo.get_by_id(site_deployment_id)
|
|
|
|
if site_deployment:
|
|
hostname = site_deployment.custom_hostname or site_deployment.pull_zone_bcdn_hostname
|
|
|
|
if hostname in config.templates.mappings:
|
|
return config.templates.mappings[hostname]
|
|
|
|
template_name = self._select_random_template()
|
|
self._persist_template_mapping(hostname, template_name)
|
|
return template_name
|
|
|
|
return self._select_random_template()
|
|
|
|
def _select_random_template(self) -> str:
|
|
"""
|
|
Randomly select a template from available templates
|
|
|
|
Returns:
|
|
Template name
|
|
"""
|
|
available = self.get_available_templates()
|
|
if not available:
|
|
return "basic"
|
|
|
|
return random.choice(available)
|
|
|
|
def _persist_template_mapping(self, hostname: str, template_name: str) -> None:
|
|
"""
|
|
Save template mapping to master.config.json
|
|
|
|
Args:
|
|
hostname: Custom hostname to map
|
|
template_name: Template to assign
|
|
"""
|
|
config_path = Path("master.config.json")
|
|
|
|
try:
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
config_data = json.load(f)
|
|
|
|
if "templates" not in config_data:
|
|
config_data["templates"] = {"default": "basic", "mappings": {}}
|
|
|
|
if "mappings" not in config_data["templates"]:
|
|
config_data["templates"]["mappings"] = {}
|
|
|
|
config_data["templates"]["mappings"][hostname] = template_name
|
|
|
|
with open(config_path, 'w', encoding='utf-8') as f:
|
|
json.dump(config_data, f, indent=2)
|
|
|
|
except Exception as e:
|
|
print(f"Warning: Failed to persist template mapping: {e}")
|
|
|
|
def format_content(
|
|
self,
|
|
content: str,
|
|
title: str,
|
|
meta_description: str,
|
|
template_name: str
|
|
) -> str:
|
|
"""
|
|
Format content using specified template
|
|
|
|
Args:
|
|
content: Raw HTML content (h2, h3, p tags)
|
|
title: Article title
|
|
meta_description: Meta description for SEO
|
|
template_name: Name of template to use
|
|
|
|
Returns:
|
|
Complete formatted HTML document
|
|
|
|
Raises:
|
|
FileNotFoundError: If template doesn't exist
|
|
"""
|
|
config = get_config()
|
|
|
|
try:
|
|
template = self.load_template(template_name)
|
|
except FileNotFoundError:
|
|
fallback_template = config.templates.default
|
|
print(f"Warning: Template '{template_name}' not found, using '{fallback_template}'")
|
|
template = self.load_template(fallback_template)
|
|
|
|
formatted_html = template.replace("{{ title }}", self._escape_html(title))
|
|
formatted_html = formatted_html.replace("{{ meta_description }}", self._escape_html(meta_description))
|
|
formatted_html = formatted_html.replace("{{ content }}", content)
|
|
|
|
return formatted_html
|
|
|
|
def _escape_html(self, text: str) -> str:
|
|
"""
|
|
Escape HTML special characters in text
|
|
|
|
Args:
|
|
text: Text to escape
|
|
|
|
Returns:
|
|
Escaped text
|
|
"""
|
|
replacements = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}
|
|
|
|
for char, escaped in replacements.items():
|
|
text = text.replace(char, escaped)
|
|
|
|
return text
|