Phase 8: Customizable PDF Templates — locale support, admin editor, seed templates
Backend: - PdfTemplate model with locale field + UNIQUE(name, locale) constraint - Migration 007: pdf_templates table + template_id FK on generated_pdfs - Template service: CRUD, Jinja2 validation, render preview with sample data - Admin endpoints: CRUD /admin/pdf-templates + POST preview - User endpoint: GET /pdf/templates (active templates list) - pdf_service: resolves template from DB by ID or falls back to default for the appropriate locale - AI generate_pdf tool accepts optional template_id - Seed script + 4 HTML template files: - Basic Report (en/ru) — general-purpose report - Medical Report (en/ru) — health-focused with disclaimers Frontend: - Admin PDF templates page with editor, locale selector, live preview (iframe), template variables reference panel - PDF page: template selector dropdown in generation form - API clients for admin CRUD + user template listing - Sidebar: admin templates link - English + Russian translations Also added Phase 9 (OAuth) and Phase 10 (Rate Limits) placeholders to GeneralPlan. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,11 @@ from app.models.user import User
|
||||
from app.services.memory_service import get_user_memories
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent.parent / "templates" / "pdf"
|
||||
jinja_env = Environment(
|
||||
_file_jinja_env = Environment(
|
||||
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
||||
autoescape=select_autoescape(["html"]),
|
||||
)
|
||||
_string_jinja_env = Environment(autoescape=select_autoescape(["html"]))
|
||||
|
||||
|
||||
async def generate_pdf_report(
|
||||
@@ -25,6 +26,7 @@ async def generate_pdf_report(
|
||||
title: str,
|
||||
document_ids: list[uuid.UUID] | None = None,
|
||||
chat_id: uuid.UUID | None = None,
|
||||
template_id: uuid.UUID | None = None,
|
||||
) -> GeneratedPdf:
|
||||
# Load user
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
@@ -52,9 +54,7 @@ async def generate_pdf_report(
|
||||
"excerpt": (doc.extracted_text or "")[:2000],
|
||||
})
|
||||
|
||||
# Render HTML
|
||||
template = jinja_env.get_template("report.html")
|
||||
html = template.render(
|
||||
template_vars = dict(
|
||||
title=title,
|
||||
user_name=user.full_name or user.username,
|
||||
generated_at=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
|
||||
@@ -63,6 +63,23 @@ async def generate_pdf_report(
|
||||
ai_summary=None,
|
||||
)
|
||||
|
||||
# Resolve template
|
||||
resolved_template_id = template_id
|
||||
if template_id:
|
||||
from app.services.pdf_template_service import get_template
|
||||
tmpl = await get_template(db, template_id)
|
||||
jinja_template = _string_jinja_env.from_string(tmpl.html_content)
|
||||
else:
|
||||
from app.services.pdf_template_service import get_default_template
|
||||
default_tmpl = await get_default_template(db)
|
||||
if default_tmpl:
|
||||
resolved_template_id = default_tmpl.id
|
||||
jinja_template = _string_jinja_env.from_string(default_tmpl.html_content)
|
||||
else:
|
||||
jinja_template = _file_jinja_env.get_template("report.html")
|
||||
|
||||
html = jinja_template.render(**template_vars)
|
||||
|
||||
# Generate PDF
|
||||
pdf_id = uuid.uuid4()
|
||||
pdf_dir = Path(settings.UPLOAD_DIR).parent / "pdfs" / str(user_id)
|
||||
@@ -73,7 +90,6 @@ async def generate_pdf_report(
|
||||
from weasyprint import HTML
|
||||
HTML(string=html).write_pdf(str(pdf_path))
|
||||
except ImportError:
|
||||
# WeasyPrint not installed — write HTML as fallback
|
||||
pdf_path = pdf_path.with_suffix(".html")
|
||||
pdf_path.write_text(html, encoding="utf-8")
|
||||
|
||||
@@ -85,6 +101,7 @@ async def generate_pdf_report(
|
||||
storage_path=str(pdf_path),
|
||||
source_document_ids=document_ids,
|
||||
source_chat_id=chat_id,
|
||||
template_id=resolved_template_id,
|
||||
)
|
||||
db.add(generated)
|
||||
await db.flush()
|
||||
|
||||
Reference in New Issue
Block a user