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