Files
Document_editor/backend/app/routers/documents.py
2026-06-15 09:18:58 +03:00

244 lines
8.3 KiB
Python

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")