import json import os import uuid from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session, joinedload from app.auth import get_current_user from app.config import settings from app.database import get_db from app.models.document import FilledDocument from app.models.template import DocumentTemplate from app.models.user import User from app.schemas import ( FilledDocumentCreate, FilledDocumentResponse, FilledDocumentUpdate, PreviewResponse, ) from app.services.access import accessible_document, accessible_template, is_admin from app.services.docx_renderer import docx_to_html, export_to_pdf, render_docx router = APIRouter(prefix="/api/documents", tags=["documents"]) def _doc_to_response(doc: FilledDocument) -> FilledDocumentResponse: field_data = json.loads(doc.field_data) return FilledDocumentResponse( id=doc.id, template_id=doc.template_id, owner_id=doc.owner_id, owner_username=doc.owner.username if doc.owner else None, name=doc.name, field_data=field_data, created_at=doc.created_at, ) def _get_template_for_document( db: Session, user: User, template_id: int ) -> DocumentTemplate: template = accessible_template(db, user, template_id) if not template: raise HTTPException(status_code=404, detail="Template not found") return template def _rerender_document(doc: FilledDocument, template: DocumentTemplate) -> None: field_data = json.loads(doc.field_data) output_path = doc.rendered_docx_path or os.path.join( settings.export_dir, f"{uuid.uuid4().hex}_rendered.docx" ) os.makedirs(settings.export_dir, exist_ok=True) render_docx(template.file_path, field_data, output_path) doc.rendered_docx_path = output_path @router.get("", response_model=list[FilledDocumentResponse]) def list_documents( db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): query = db.query(FilledDocument).options(joinedload(FilledDocument.owner)) if not is_admin(current_user): query = query.filter(FilledDocument.owner_id == current_user.id) docs = query.order_by(FilledDocument.created_at.desc()).all() return [_doc_to_response(d) for d in docs] @router.get("/{document_id}", response_model=FilledDocumentResponse) def get_document( document_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = ( db.query(FilledDocument) .options(joinedload(FilledDocument.owner)) .filter(FilledDocument.id == document_id) .first() ) if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") return _doc_to_response(doc) @router.patch("/{document_id}", response_model=FilledDocumentResponse) def update_document( document_id: int, update: FilledDocumentUpdate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = ( db.query(FilledDocument) .options(joinedload(FilledDocument.owner)) .filter(FilledDocument.id == document_id) .first() ) if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if update.name is not None: doc.name = update.name if update.field_data is not None: doc.field_data = json.dumps(update.field_data) template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template not found") _rerender_document(doc, template) db.commit() db.refresh(doc) return _doc_to_response(doc) @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_document( document_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if doc.rendered_docx_path and os.path.exists(doc.rendered_docx_path): os.remove(doc.rendered_docx_path) db.delete(doc) db.commit() @router.post("", response_model=FilledDocumentResponse, status_code=status.HTTP_201_CREATED) def create_document( data: FilledDocumentCreate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): template = _get_template_for_document(db, current_user, data.template_id) os.makedirs(settings.export_dir, exist_ok=True) output_name = f"{uuid.uuid4().hex}_rendered.docx" output_path = os.path.join(settings.export_dir, output_name) render_docx(template.file_path, data.field_data, output_path) doc = FilledDocument( template_id=template.id, owner_id=current_user.id, name=data.name, field_data=json.dumps(data.field_data), rendered_docx_path=output_path, ) db.add(doc) db.commit() db.refresh(doc) return _doc_to_response(doc) @router.post("/preview", response_model=PreviewResponse) def preview_document( data: FilledDocumentCreate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): template = _get_template_for_document(db, current_user, data.template_id) os.makedirs(settings.export_dir, exist_ok=True) temp_path = os.path.join(settings.export_dir, f"preview_{uuid.uuid4().hex}.docx") try: render_docx(template.file_path, data.field_data, temp_path) html = docx_to_html(temp_path) return PreviewResponse(html=html, field_data=data.field_data) finally: if os.path.exists(temp_path): os.remove(temp_path) @router.get("/{document_id}/export/docx") def export_docx( document_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if not doc.rendered_docx_path or not os.path.exists(doc.rendered_docx_path): template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if template: _rerender_document(doc, template) db.commit() if not doc.rendered_docx_path: raise HTTPException(status_code=404, detail="Rendered file not found") return FileResponse( doc.rendered_docx_path, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", filename=f"{doc.name}.docx", ) @router.get("/{document_id}/export/pdf") def export_pdf( document_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if not doc.rendered_docx_path or not os.path.exists(doc.rendered_docx_path): template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template not found") _rerender_document(doc, template) db.commit() docx_path = doc.rendered_docx_path if not docx_path: raise HTTPException(status_code=404, detail="Rendered file not found") pdf_path = docx_path.replace(".docx", ".pdf") try: export_to_pdf(docx_path, pdf_path) except RuntimeError as e: raise HTTPException(status_code=500, detail=str(e)) return FileResponse(pdf_path, media_type="application/pdf", filename=f"{doc.name}.pdf")