301 lines
9.6 KiB
Python
301 lines
9.6 KiB
Python
import json
|
|
import os
|
|
import uuid
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, 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.audit import get_client_ip, log_action
|
|
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,
|
|
request: Request,
|
|
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)
|
|
log_action(
|
|
db,
|
|
"document.update",
|
|
user=current_user,
|
|
resource_type="document",
|
|
resource_id=doc.id,
|
|
details={"name": doc.name},
|
|
ip_address=get_client_ip(request),
|
|
)
|
|
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,
|
|
request: Request,
|
|
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")
|
|
|
|
log_action(
|
|
db,
|
|
"document.delete",
|
|
user=current_user,
|
|
resource_type="document",
|
|
resource_id=doc.id,
|
|
details={"name": doc.name},
|
|
ip_address=get_client_ip(request),
|
|
)
|
|
|
|
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,
|
|
request: Request,
|
|
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.flush()
|
|
log_action(
|
|
db,
|
|
"document.create",
|
|
user=current_user,
|
|
resource_type="document",
|
|
resource_id=doc.id,
|
|
details={"name": doc.name, "template_id": doc.template_id},
|
|
ip_address=get_client_ip(request),
|
|
)
|
|
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,
|
|
request: Request,
|
|
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")
|
|
|
|
log_action(
|
|
db,
|
|
"document.export_docx",
|
|
user=current_user,
|
|
resource_type="document",
|
|
resource_id=doc.id,
|
|
details={"name": doc.name},
|
|
ip_address=get_client_ip(request),
|
|
)
|
|
db.commit()
|
|
|
|
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,
|
|
request: Request,
|
|
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))
|
|
|
|
log_action(
|
|
db,
|
|
"document.export_pdf",
|
|
user=current_user,
|
|
resource_type="document",
|
|
resource_id=doc.id,
|
|
details={"name": doc.name},
|
|
ip_address=get_client_ip(request),
|
|
)
|
|
db.commit()
|
|
|
|
return FileResponse(pdf_path, media_type="application/pdf", filename=f"{doc.name}.pdf")
|