Files
govoplan-forms/src/govoplan_forms/backend/router.py
T

244 lines
7.9 KiB
Python

from __future__ import annotations
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.institutional import InstitutionalContextError
from govoplan_core.db.session import get_session
from govoplan_forms.backend.manifest import ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE
from govoplan_forms.backend.schemas import (
FormDefinitionHistoryResponse,
FormDefinitionListResponse,
FormDefinitionWriteRequest,
FormPackageImportRequest,
FormPackageRequest,
)
from govoplan_forms.backend.service import (
FormDefinitionStoreError,
definition_from_mapping,
assess_form_definition_fragment,
export_form_definition_fragment,
form_definition_diagnostics,
form_definition_history,
get_form_definition,
list_form_definitions,
import_form_definition_fragment,
record_form_definition,
)
router = APIRouter(prefix="/forms", tags=["forms"])
def _require(principal: ApiPrincipal, scope: str) -> None:
if not has_scope(principal, scope):
raise HTTPException(status_code=403, detail=f"Missing scope: {scope}")
def _error(exc: Exception) -> HTTPException:
message = str(exc)
code = (
409
if any(word in message.casefold() for word in ("conflict", "already", "stale"))
else 400
)
return HTTPException(status_code=code, detail=message)
@router.get("/definitions", response_model=FormDefinitionListResponse)
def api_list_form_definitions(
q: str = Query(default="", max_length=200),
publication_state: list[str] | None = Query(default=None),
offset: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=200),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> FormDefinitionListResponse:
_require(principal, READ_SCOPE)
try:
items, total = list_form_definitions(
session,
principal,
query=q,
publication_states=publication_state,
offset=offset,
limit=limit,
)
except FormDefinitionStoreError as exc:
raise _error(exc) from exc
return FormDefinitionListResponse(
definitions=[item.to_dict() for item in items],
total=total,
)
@router.put(
"/definitions/{form_id}",
response_model=dict[str, object],
status_code=status.HTTP_200_OK,
)
def api_record_form_definition(
form_id: str,
payload: FormDefinitionWriteRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, WRITE_SCOPE)
try:
definition = definition_from_mapping(payload.definition)
if definition.reference.object_id != form_id:
raise FormDefinitionStoreError(
"Form definition path and payload IDs must match."
)
if definition.publication_state != "draft":
_require(principal, ADMIN_SCOPE)
stored = record_form_definition(
session,
principal,
definition=definition,
expected_revision=payload.expected_revision,
)
session.commit()
except (FormDefinitionStoreError, InstitutionalContextError) as exc:
session.rollback()
raise _error(exc) from exc
return stored.to_dict()
@router.get("/definitions/{form_id}", response_model=dict[str, object])
def api_get_form_definition(
form_id: str,
revision: str | None = Query(default=None, max_length=255),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, READ_SCOPE)
item = get_form_definition(
session,
principal,
form_id=form_id,
revision=revision,
)
if item is None:
raise HTTPException(status_code=404, detail="Form definition not found")
return item.to_dict()
@router.get("/definitions/{form_id}/diagnostics", response_model=dict[str, object])
def api_form_definition_diagnostics(
form_id: str,
revision: str | None = Query(default=None, max_length=255),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, READ_SCOPE)
item = get_form_definition(session, principal, form_id=form_id, revision=revision)
if item is None:
raise HTTPException(status_code=404, detail="Form definition not found")
diagnostics = form_definition_diagnostics(item)
return {
"diagnostics": [dict(value) for value in diagnostics],
"error_count": sum(value.get("severity") == "error" for value in diagnostics),
"warning_count": sum(
value.get("severity") == "warning" for value in diagnostics
),
}
@router.get("/definitions/{form_id}/package", response_model=dict[str, object])
def api_export_form_definition_package(
form_id: str,
revision: str | None = Query(default=None, max_length=255),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, READ_SCOPE)
item = get_form_definition(session, principal, form_id=form_id, revision=revision)
if item is None:
raise HTTPException(status_code=404, detail="Form definition not found")
actor_id = next(
(
str(getattr(principal, name))
for name in ("account_id", "identity_id", "membership_id")
if getattr(principal, name, None)
),
None,
)
return export_form_definition_fragment(
item,
exported_at=datetime.now(UTC),
exported_by=actor_id,
)
@router.post("/packages/assess", response_model=dict[str, object])
def api_assess_form_definition_package(
payload: FormPackageRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, WRITE_SCOPE)
try:
return assess_form_definition_fragment(
session,
principal,
fragment=payload.fragment,
)
except (FormDefinitionStoreError, InstitutionalContextError) as exc:
raise _error(exc) from exc
@router.post("/packages/import", response_model=dict[str, object])
def api_import_form_definition_package(
payload: FormPackageImportRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, WRITE_SCOPE)
try:
item = import_form_definition_fragment(
session,
principal,
fragment=payload.fragment,
target_form_id=payload.target_form_id,
target_key=payload.target_key,
expected_revision=payload.expected_revision,
change_reason=payload.change_reason,
recorded_at=payload.recorded_at,
)
session.commit()
except (FormDefinitionStoreError, InstitutionalContextError) as exc:
session.rollback()
raise _error(exc) from exc
return item.to_dict()
@router.get(
"/definitions/{form_id}/history",
response_model=FormDefinitionHistoryResponse,
)
def api_form_definition_history(
form_id: str,
limit: int = Query(default=100, ge=1, le=200),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> FormDefinitionHistoryResponse:
_require(principal, READ_SCOPE)
return FormDefinitionHistoryResponse(
revisions=[
item.to_dict()
for item in form_definition_history(
session,
principal,
form_id=form_id,
limit=limit,
)
]
)
__all__ = ["router"]