Files
Document_editor/backend/app/routers/templates.py

320 lines
9.9 KiB
Python

import json
import os
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile, 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.template import DocumentTemplate, TemplateVariable
from app.models.user import User
from app.schemas import (
DocumentTemplateListItem,
DocumentTemplateResponse,
TemplateUpdate,
TemplateVariableResponse,
TemplateVariableUpdate,
)
from app.services.access import (
accessible_template,
accessible_templates_query,
can_manage_template,
is_admin,
manageable_template,
)
from app.services.audit import get_client_ip, log_action
from app.services.docx_parser import parse_docx_template
router = APIRouter(prefix="/api/templates", tags=["templates"])
def _owner_username(template: DocumentTemplate) -> str | None:
return template.owner.username if template.owner else None
def _template_to_response(template: DocumentTemplate, current_user: User) -> DocumentTemplateResponse:
variables = []
for var in template.variables:
style = None
if var.style_params:
try:
style = json.loads(var.style_params)
except json.JSONDecodeError:
style = None
variables.append(
TemplateVariableResponse(
id=var.id,
name=var.name,
label=var.label,
field_type=var.field_type,
default_value=var.default_value,
is_required=var.is_required,
order=var.order,
parent_variable=var.parent_variable,
style_params=style,
)
)
return DocumentTemplateResponse(
id=template.id,
name=template.name,
description=template.description,
original_filename=template.original_filename,
owner_id=template.owner_id,
owner_username=_owner_username(template),
is_public=template.is_public,
is_owner=template.owner_id == current_user.id,
created_at=template.created_at,
updated_at=template.updated_at,
variables=variables,
)
def _template_to_list_item(template: DocumentTemplate, current_user: User) -> DocumentTemplateListItem:
return DocumentTemplateListItem(
id=template.id,
name=template.name,
description=template.description,
original_filename=template.original_filename,
owner_id=template.owner_id,
owner_username=_owner_username(template),
is_public=template.is_public,
is_owner=template.owner_id == current_user.id,
created_at=template.created_at,
variable_count=len(template.variables),
)
@router.get("", response_model=list[DocumentTemplateListItem])
def list_templates(
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
if is_admin(current_user):
templates = (
db.query(DocumentTemplate)
.options(joinedload(DocumentTemplate.owner))
.order_by(DocumentTemplate.created_at.desc())
.all()
)
else:
templates = (
accessible_templates_query(db, current_user)
.options(joinedload(DocumentTemplate.owner))
.order_by(DocumentTemplate.created_at.desc())
.all()
)
return [_template_to_list_item(t, current_user) for t in templates]
@router.get("/{template_id}", response_model=DocumentTemplateResponse)
def get_template(
template_id: int,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
template = accessible_template(db, current_user, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
return _template_to_response(template, current_user)
@router.get("/{template_id}/download")
def download_template_source(
template_id: int,
request: Request,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
template = accessible_template(db, current_user, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
if not os.path.exists(template.file_path):
raise HTTPException(status_code=404, detail="Source file not found")
log_action(
db,
"template.download",
user=current_user,
resource_type="template",
resource_id=template.id,
details={"name": template.name},
ip_address=get_client_ip(request),
)
db.commit()
return FileResponse(
template.file_path,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
filename=template.original_filename,
)
@router.patch("/{template_id}", response_model=DocumentTemplateResponse)
def update_template(
template_id: int,
update: TemplateUpdate,
request: Request,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
template = manageable_template(db, current_user, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
changes = update.model_dump(exclude_unset=True)
for field, value in changes.items():
setattr(template, field, value)
log_action(
db,
"template.update",
user=current_user,
resource_type="template",
resource_id=template.id,
details={"name": template.name, "changes": changes},
ip_address=get_client_ip(request),
)
db.commit()
db.refresh(template)
return _template_to_response(template, current_user)
@router.post("", response_model=DocumentTemplateResponse, status_code=status.HTTP_201_CREATED)
async def create_template(
request: Request,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
file: UploadFile = File(...),
name: str = Form(...),
description: str | None = Form(None),
is_public: bool = Form(False),
):
if not file.filename or not file.filename.endswith(".docx"):
raise HTTPException(status_code=400, detail="Only .docx files are supported")
os.makedirs(settings.upload_dir, exist_ok=True)
unique_name = f"{uuid.uuid4().hex}_{file.filename}"
file_path = os.path.join(settings.upload_dir, unique_name)
content = await file.read()
with open(file_path, "wb") as f:
f.write(content)
parsed_vars = parse_docx_template(file_path)
template = DocumentTemplate(
name=name,
description=description,
original_filename=file.filename,
file_path=file_path,
owner_id=current_user.id,
is_public=is_public,
)
db.add(template)
db.flush()
for pv in parsed_vars:
db.add(
TemplateVariable(
template_id=template.id,
name=pv.name,
label=pv.label,
field_type=pv.field_type,
default_value=pv.default_value,
is_required=pv.is_required,
order=pv.order,
parent_variable=pv.parent_variable,
style_params=json.dumps(pv.style_params) if pv.style_params else None,
)
)
log_action(
db,
"template.create",
user=current_user,
resource_type="template",
resource_id=template.id,
details={"name": template.name, "is_public": template.is_public},
ip_address=get_client_ip(request),
)
db.commit()
db.refresh(template)
return _template_to_response(template, current_user)
@router.put("/{template_id}/variables/{variable_id}", response_model=TemplateVariableResponse)
def update_variable(
template_id: int,
variable_id: int,
update: TemplateVariableUpdate,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
template = manageable_template(db, current_user, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
variable = db.query(TemplateVariable).filter(
TemplateVariable.id == variable_id,
TemplateVariable.template_id == template_id,
).first()
if not variable:
raise HTTPException(status_code=404, detail="Variable not found")
for field, value in update.model_dump(exclude_unset=True).items():
setattr(variable, field, value)
db.commit()
db.refresh(variable)
style = None
if variable.style_params:
try:
style = json.loads(variable.style_params)
except json.JSONDecodeError:
pass
return TemplateVariableResponse(
id=variable.id,
name=variable.name,
label=variable.label,
field_type=variable.field_type,
default_value=variable.default_value,
is_required=variable.is_required,
order=variable.order,
parent_variable=variable.parent_variable,
style_params=style,
)
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_template(
template_id: int,
request: Request,
db: Annotated[Session, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_user)],
):
template = manageable_template(db, current_user, template_id)
if not template:
raise HTTPException(status_code=404, detail="Template not found")
log_action(
db,
"template.delete",
user=current_user,
resource_type="template",
resource_id=template.id,
details={"name": template.name},
ip_address=get_client_ip(request),
)
if os.path.exists(template.file_path):
os.remove(template.file_path)
db.delete(template)
db.commit()