Files
personal-ai-assistant/backend/app/api/v1/pdf.py
dolgolyov.alexei bb53eeee8e 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>
2026-03-19 15:32:35 +03:00

65 lines
2.4 KiB
Python

import uuid
from pathlib import Path
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.database import get_db
from app.models.user import User
from app.schemas.pdf import GeneratePdfRequest, PdfListResponse, PdfResponse
from app.schemas.pdf_template import PdfTemplateSummaryListResponse, PdfTemplateSummaryResponse
from app.services import pdf_service, pdf_template_service
router = APIRouter(prefix="/pdf", tags=["pdf"])
@router.get("/templates", response_model=PdfTemplateSummaryListResponse)
async def list_available_templates(
_user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
templates = await pdf_template_service.list_templates(db, active_only=True)
return PdfTemplateSummaryListResponse(
templates=[PdfTemplateSummaryResponse.model_validate(t) for t in templates]
)
@router.post("/compile", response_model=PdfResponse, status_code=status.HTTP_201_CREATED)
async def compile_pdf(
data: GeneratePdfRequest,
user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
pdf = await pdf_service.generate_pdf_report(
db, user.id, data.title, data.document_ids or None, data.chat_id, data.template_id,
)
return PdfResponse.model_validate(pdf)
@router.get("/", response_model=PdfListResponse)
async def list_pdfs(
user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
pdfs = await pdf_service.get_user_pdfs(db, user.id)
return PdfListResponse(pdfs=[PdfResponse.model_validate(p) for p in pdfs])
@router.get("/{pdf_id}/download")
async def download_pdf(
pdf_id: uuid.UUID,
user: Annotated[User, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
pdf = await pdf_service.get_pdf(db, pdf_id, user.id)
if not pdf:
raise HTTPException(status_code=404, detail="PDF not found")
file_path = Path(pdf.storage_path)
if not file_path.exists():
raise HTTPException(status_code=404, detail="PDF file not found on disk")
media_type = "application/pdf" if file_path.suffix == ".pdf" else "text/html"
return FileResponse(path=str(file_path), filename=f"{pdf.title}.pdf", media_type=media_type)