from typing import Any from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.orm import Session from datetime import datetime, timezone import json from app.db import get_db from app.models.document import Document from app.models.annotation_state import AnnotationState from app.auth.dependencies import get_current_user, verify_csrf from app.schemas.annotations import AnnotationStateResponse, AnnotationStateUpdateRequest, AnnotationStateUpdateResponse router = APIRouter(prefix="/documents", tags=["Annotations"]) def _now_iso() -> str: return datetime.now(timezone.utc).isoformat() @router.get("/{document_id}/annotations", response_model=AnnotationStateResponse) async def get_annotations( document_id: str, db: Session = Depends(get_db), user_id: Any = Depends(get_current_user) ): doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None))) if not doc: raise HTTPException(status_code=404, detail="Document not found") state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id)) if not state: return AnnotationStateResponse(data=[], updatedAt=datetime.now(timezone.utc)) try: data = json.loads(state.data) except json.JSONDecodeError: data = [] return AnnotationStateResponse( data=data, updatedAt=datetime.fromisoformat(state.updated_at) ) @router.put("/{document_id}/annotations", response_model=AnnotationStateUpdateResponse, dependencies=[Depends(verify_csrf)]) async def update_annotations( document_id: str, request: AnnotationStateUpdateRequest, db: Session = Depends(get_db), user_id: Any = Depends(get_current_user) ): doc = db.scalar(select(Document).where(Document.id == document_id, Document.deleted_at.is_(None))) if not doc: raise HTTPException(status_code=404, detail="Document not found") state = db.scalar(select(AnnotationState).where(AnnotationState.document_id == document_id)) # Check for stale tab if state and request.baseUpdatedAt: current_updated_at = datetime.fromisoformat(state.updated_at).replace(tzinfo=timezone.utc) request_updated_at = request.baseUpdatedAt.replace(tzinfo=timezone.utc) # Allow a small grace period for timezone/parsing discrepancies if abs((current_updated_at - request_updated_at).total_seconds()) > 1.0: raise HTTPException( status_code=409, detail=f"Conflict: annotations have been modified since {request.baseUpdatedAt}" ) now = _now_iso() import json # serialize request.data list of models to json string data_json = json.dumps([a.model_dump(mode="json", by_alias=True) for a in request.data]) if not state: state = AnnotationState( document_id=document_id, data=data_json, updated_at=now ) db.add(state) else: state.data = data_json state.updated_at = now # Bump document updated_at doc.updated_at = now db.commit() return AnnotationStateUpdateResponse(updatedAt=datetime.fromisoformat(now))