Paperjet/backend/app/api/v1/annotations.py
Elijah 4cb038ec78
Some checks failed
Automated Container Build / build-and-push (push) Failing after 4s
CI / Backend (Python) (push) Failing after 17s
CI / Frontend (TypeScript) (push) Failing after 4m48s
Phase 3 implementation. Coordinate system implemented. Initial text box tool implemented.
2026-06-12 16:30:07 -07:00

91 lines
3.2 KiB
Python

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