244 lines
7.9 KiB
Python
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"]
|