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:
2026-03-19 15:32:35 +03:00
parent b0790d719c
commit bb53eeee8e
26 changed files with 1077 additions and 16 deletions

View File

@@ -0,0 +1,51 @@
"""Seed default PDF templates (basic + medical, en + ru)."""
import asyncio
from pathlib import Path
from sqlalchemy import select
from app.database import async_session_factory
from app.models.pdf_template import PdfTemplate
TEMPLATES_DIR = Path(__file__).parent.parent / "app" / "templates" / "pdf" / "seeds"
async def seed_templates() -> None:
templates = [
("Basic Report", "en", "Standard report with key information and documents", "basic_en.html", True),
("Basic Report", "ru", "Стандартный отчёт с ключевой информацией и документами", "basic_ru.html", True),
("Medical Report", "en", "Medical-styled report with health profile and disclaimers", "medical_en.html", False),
("Medical Report", "ru", "Медицинский отчёт с профилем здоровья и дисклеймером", "medical_ru.html", False),
]
async with async_session_factory() as db:
for name, locale, desc, filename, is_default in templates:
result = await db.execute(
select(PdfTemplate).where(PdfTemplate.name == name, PdfTemplate.locale == locale)
)
if result.scalar_one_or_none():
print(f" Exists: {name} ({locale})")
continue
html_path = TEMPLATES_DIR / filename
if not html_path.exists():
print(f" Missing: {html_path}")
continue
html_content = html_path.read_text(encoding="utf-8")
template = PdfTemplate(
name=name,
locale=locale,
description=desc,
html_content=html_content,
is_default=is_default,
)
db.add(template)
print(f" Created: {name} ({locale})")
await db.commit()
print("Template seeding complete.")
if __name__ == "__main__":
asyncio.run(seed_templates())