feat: implement immutable form definitions
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
)
|
||||
from govoplan_forms.backend.service import (
|
||||
FormDefinitionStoreError,
|
||||
definition_from_mapping,
|
||||
form_definition_history,
|
||||
get_form_definition,
|
||||
list_form_definitions,
|
||||
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}/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"]
|
||||
Reference in New Issue
Block a user