Initialize governed Views module
This commit is contained in:
713
src/govoplan_views/backend/router.py
Normal file
713
src/govoplan_views/backend/router.py
Normal file
@@ -0,0 +1,713 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
|
||||
from govoplan_core.core.views import VIEW_SURFACE_CONTRACT_VERSION, ViewSurface
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_views.backend.manifest import (
|
||||
ASSIGNMENT_READ_SCOPE,
|
||||
ASSIGNMENT_WRITE_SCOPE,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
SELECTION_READ_SCOPE,
|
||||
SELECTION_WRITE_SCOPE,
|
||||
SYSTEM_ASSIGNMENT_READ_SCOPE,
|
||||
SYSTEM_ASSIGNMENT_WRITE_SCOPE,
|
||||
SYSTEM_DEFINITION_READ_SCOPE,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
from govoplan_views.backend.runtime import get_registry
|
||||
from govoplan_views.backend.schemas import (
|
||||
EffectiveViewOptionResponse,
|
||||
EffectiveViewResponse,
|
||||
ViewDiagnosticResponse,
|
||||
ViewAssignmentCreateRequest,
|
||||
ViewAssignmentListResponse,
|
||||
ViewAssignmentResponse,
|
||||
ViewAssignmentUpdateRequest,
|
||||
ViewDefinitionCreateRequest,
|
||||
ViewDefinitionListResponse,
|
||||
ViewDefinitionResponse,
|
||||
ViewDefinitionUpdateRequest,
|
||||
ViewProvenanceResponse,
|
||||
ViewRevisionCreateRequest,
|
||||
ViewRevisionResponse,
|
||||
ViewSelectionRequest,
|
||||
ViewSurfaceCatalogueResponse,
|
||||
ViewSurfaceResponse,
|
||||
)
|
||||
from govoplan_views.backend.service import (
|
||||
EffectiveViewState,
|
||||
ViewsConflictError,
|
||||
ViewsError,
|
||||
ViewsNotFoundError,
|
||||
ViewsValidationError,
|
||||
archive_definition,
|
||||
assignment_payload,
|
||||
create_assignment,
|
||||
create_definition,
|
||||
create_revision,
|
||||
definition_payload,
|
||||
definition_revisions,
|
||||
delete_assignment,
|
||||
get_assignment,
|
||||
get_definition,
|
||||
get_revision,
|
||||
list_assignments,
|
||||
list_definitions,
|
||||
publish_revision,
|
||||
resolve_effective_view,
|
||||
select_view,
|
||||
update_assignment,
|
||||
update_definition,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/views", tags=["views"])
|
||||
|
||||
|
||||
def _catalogue() -> tuple[ViewSurface, ...]:
|
||||
return get_registry().view_surfaces()
|
||||
|
||||
|
||||
def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
if any(has_scope(principal, scope) for scope in scopes):
|
||||
return
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Requires one of: {', '.join(scopes)}",
|
||||
)
|
||||
|
||||
|
||||
def _require_definition_read(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SYSTEM_DEFINITION_READ_SCOPE,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
_require_any_scope(
|
||||
principal,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
|
||||
|
||||
def _require_definition_write(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE
|
||||
if scope_type == "system"
|
||||
else DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
|
||||
|
||||
def _require_assignment_read(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
if scope_type == "system":
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SYSTEM_ASSIGNMENT_READ_SCOPE,
|
||||
SYSTEM_ASSIGNMENT_WRITE_SCOPE,
|
||||
)
|
||||
return
|
||||
_require_any_scope(
|
||||
principal,
|
||||
ASSIGNMENT_READ_SCOPE,
|
||||
ASSIGNMENT_WRITE_SCOPE,
|
||||
)
|
||||
|
||||
|
||||
def _require_assignment_write(principal: ApiPrincipal, scope_type: str) -> None:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SYSTEM_ASSIGNMENT_WRITE_SCOPE
|
||||
if scope_type == "system"
|
||||
else ASSIGNMENT_WRITE_SCOPE,
|
||||
)
|
||||
|
||||
|
||||
def _actor_id(principal: ApiPrincipal) -> str:
|
||||
return principal.account_id
|
||||
|
||||
|
||||
def _http_error(exc: ViewsError) -> HTTPException:
|
||||
if isinstance(exc, ViewsNotFoundError):
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc))
|
||||
if isinstance(exc, ViewsConflictError):
|
||||
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
|
||||
if isinstance(exc, ViewsValidationError):
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
)
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
|
||||
def _audit(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
action: str,
|
||||
object_type: str,
|
||||
object_id: str,
|
||||
details: dict[str, object],
|
||||
) -> None:
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action=action,
|
||||
object_type=object_type,
|
||||
object_id=object_id,
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
def _effective_response(state: EffectiveViewState) -> EffectiveViewResponse:
|
||||
effective = state.effective
|
||||
return EffectiveViewResponse(
|
||||
active_view_id=effective.view_id,
|
||||
active_revision_id=effective.revision_id,
|
||||
active_view_name=effective.name,
|
||||
visible_surface_ids=sorted(effective.visible_surface_ids),
|
||||
locked=effective.locked,
|
||||
available_views=[
|
||||
EffectiveViewOptionResponse(
|
||||
id=option.id,
|
||||
name=option.name,
|
||||
description=option.description,
|
||||
revision_id=option.revision_id,
|
||||
)
|
||||
for option in state.available_views
|
||||
],
|
||||
provenance=[
|
||||
ViewProvenanceResponse.model_validate(item) for item in effective.provenance
|
||||
],
|
||||
diagnostics=[
|
||||
ViewDiagnosticResponse(
|
||||
severity=item.severity,
|
||||
code=item.code,
|
||||
message=item.message,
|
||||
surface_ids=list(item.surface_ids),
|
||||
)
|
||||
for item in state.diagnostics
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _definition_response(
|
||||
session: Session,
|
||||
definition,
|
||||
*,
|
||||
readonly: bool,
|
||||
) -> ViewDefinitionResponse:
|
||||
return ViewDefinitionResponse.model_validate(
|
||||
definition_payload(
|
||||
session,
|
||||
definition,
|
||||
readonly=readonly,
|
||||
catalogue=_catalogue(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/effective", response_model=EffectiveViewResponse)
|
||||
def api_effective_view(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> EffectiveViewResponse:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
SELECTION_READ_SCOPE,
|
||||
SELECTION_WRITE_SCOPE,
|
||||
)
|
||||
return _effective_response(
|
||||
resolve_effective_view(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
account_id=principal.account_id,
|
||||
group_ids=principal.group_ids,
|
||||
catalogue=_catalogue(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.put("/selection", response_model=EffectiveViewResponse)
|
||||
def api_select_view(
|
||||
payload: ViewSelectionRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> EffectiveViewResponse:
|
||||
_require_any_scope(principal, SELECTION_WRITE_SCOPE)
|
||||
try:
|
||||
state = select_view(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
account_id=principal.account_id,
|
||||
group_ids=principal.group_ids,
|
||||
view_id=payload.view_id,
|
||||
catalogue=_catalogue(),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.selection.update",
|
||||
object_type="view_preference",
|
||||
object_id=principal.account_id,
|
||||
details={"view_id": payload.view_id},
|
||||
)
|
||||
session.commit()
|
||||
return _effective_response(state)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/surfaces", response_model=ViewSurfaceCatalogueResponse)
|
||||
def api_view_surfaces(
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewSurfaceCatalogueResponse:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
DEFINITION_READ_SCOPE,
|
||||
DEFINITION_WRITE_SCOPE,
|
||||
SYSTEM_DEFINITION_READ_SCOPE,
|
||||
SYSTEM_DEFINITION_WRITE_SCOPE,
|
||||
)
|
||||
lockout_ids = {
|
||||
"access.module",
|
||||
"access.nav.admin",
|
||||
"access.route.admin",
|
||||
"views.module",
|
||||
"views.selector",
|
||||
"views.admin.system",
|
||||
"views.admin.tenant",
|
||||
}
|
||||
return ViewSurfaceCatalogueResponse(
|
||||
contract_version=VIEW_SURFACE_CONTRACT_VERSION,
|
||||
surfaces=[
|
||||
ViewSurfaceResponse(
|
||||
id=surface.id,
|
||||
module_id=surface.module_id,
|
||||
kind=surface.kind,
|
||||
label=surface.label,
|
||||
parent_id=surface.parent_id,
|
||||
description=surface.description,
|
||||
order=surface.order,
|
||||
default_visible=surface.default_visible,
|
||||
required=surface.required,
|
||||
required_for_locked_view=surface.id in lockout_ids,
|
||||
)
|
||||
for surface in _catalogue()
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/definitions", response_model=ViewDefinitionListResponse)
|
||||
def api_list_definitions(
|
||||
scope_type: str = Query(default="tenant", pattern="^(system|tenant)$"),
|
||||
include_inherited: bool = True,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionListResponse:
|
||||
_require_definition_read(principal, scope_type)
|
||||
definitions = list_definitions(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=scope_type,
|
||||
include_inherited=include_inherited,
|
||||
)
|
||||
return ViewDefinitionListResponse(
|
||||
definitions=[
|
||||
_definition_response(
|
||||
session,
|
||||
definition,
|
||||
readonly=(scope_type == "tenant" and definition.scope_type == "system"),
|
||||
)
|
||||
for definition in definitions
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitions",
|
||||
response_model=ViewDefinitionResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_definition(
|
||||
payload: ViewDefinitionCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
_require_definition_write(principal, payload.scope_type)
|
||||
try:
|
||||
definition = create_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=payload.scope_type,
|
||||
definition_key=payload.definition_key,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
visible_surface_ids=payload.visible_surface_ids,
|
||||
catalogue=_catalogue(),
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.definition.create",
|
||||
object_type="view_definition",
|
||||
object_id=definition.id,
|
||||
details={"scope_type": definition.scope_type},
|
||||
)
|
||||
session.commit()
|
||||
return _definition_response(session, definition, readonly=False)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/definitions/{definition_id}",
|
||||
response_model=ViewDefinitionResponse,
|
||||
)
|
||||
def api_update_definition(
|
||||
definition_id: str,
|
||||
payload: ViewDefinitionUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
update_definition(
|
||||
session,
|
||||
definition,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
actor_id=_actor_id(principal),
|
||||
fields_set=payload.model_fields_set,
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.definition.update",
|
||||
object_type="view_definition",
|
||||
object_id=definition.id,
|
||||
details={"fields": sorted(payload.model_fields_set)},
|
||||
)
|
||||
session.commit()
|
||||
return _definition_response(session, definition, readonly=False)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/definitions/{definition_id}/revisions",
|
||||
response_model=list[ViewRevisionResponse],
|
||||
)
|
||||
def api_list_revisions(
|
||||
definition_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> list[ViewRevisionResponse]:
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_read(principal, definition.scope_type)
|
||||
return [
|
||||
ViewRevisionResponse.model_validate(revision)
|
||||
for revision in definition_revisions(
|
||||
session,
|
||||
definition_id=definition.id,
|
||||
)
|
||||
]
|
||||
except ViewsError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitions/{definition_id}/revisions",
|
||||
response_model=ViewDefinitionResponse,
|
||||
)
|
||||
def api_create_revision(
|
||||
definition_id: str,
|
||||
payload: ViewRevisionCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
revision = create_revision(
|
||||
session,
|
||||
definition,
|
||||
visible_surface_ids=payload.visible_surface_ids,
|
||||
catalogue=_catalogue(),
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.revision.create",
|
||||
object_type="view_definition",
|
||||
object_id=definition.id,
|
||||
details={
|
||||
"revision": revision.revision,
|
||||
"content_hash": revision.content_hash,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return _definition_response(session, definition, readonly=False)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitions/{definition_id}/revisions/{revision_id}/publish",
|
||||
response_model=ViewDefinitionResponse,
|
||||
)
|
||||
def api_publish_revision(
|
||||
definition_id: str,
|
||||
revision_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
revision = get_revision(
|
||||
session,
|
||||
definition_id=definition.id,
|
||||
revision_id=revision_id,
|
||||
)
|
||||
publish_revision(
|
||||
session,
|
||||
definition,
|
||||
revision,
|
||||
catalogue=_catalogue(),
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.revision.publish",
|
||||
object_type="view_definition",
|
||||
object_id=definition.id,
|
||||
details={"revision": revision.revision, "revision_id": revision.id},
|
||||
)
|
||||
session.commit()
|
||||
return _definition_response(session, definition, readonly=False)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/definitions/{definition_id}",
|
||||
response_model=ViewDefinitionResponse,
|
||||
)
|
||||
def api_archive_definition(
|
||||
definition_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewDefinitionResponse:
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=definition_id,
|
||||
)
|
||||
_require_definition_write(principal, definition.scope_type)
|
||||
archive_definition(
|
||||
session,
|
||||
definition,
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.definition.archive",
|
||||
object_type="view_definition",
|
||||
object_id=definition.id,
|
||||
details={},
|
||||
)
|
||||
session.commit()
|
||||
return _definition_response(session, definition, readonly=False)
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.get("/assignments", response_model=ViewAssignmentListResponse)
|
||||
def api_list_assignments(
|
||||
scope_type: str = Query(default="tenant", pattern="^(system|tenant)$"),
|
||||
include_inherited: bool = True,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewAssignmentListResponse:
|
||||
_require_assignment_read(principal, scope_type)
|
||||
assignments = list_assignments(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=scope_type,
|
||||
include_inherited=include_inherited,
|
||||
)
|
||||
return ViewAssignmentListResponse(
|
||||
assignments=[
|
||||
ViewAssignmentResponse.model_validate(assignment_payload(assignment))
|
||||
for assignment in assignments
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/assignments",
|
||||
response_model=ViewAssignmentResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_create_assignment(
|
||||
payload: ViewAssignmentCreateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewAssignmentResponse:
|
||||
_require_assignment_write(principal, payload.scope_type)
|
||||
try:
|
||||
definition = get_definition(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
definition_id=payload.definition_id,
|
||||
)
|
||||
assignment = create_assignment(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
definition=definition,
|
||||
revision_id=payload.revision_id,
|
||||
mode=payload.mode,
|
||||
priority=payload.priority,
|
||||
is_active=payload.is_active,
|
||||
metadata=payload.metadata,
|
||||
catalogue=_catalogue(),
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.assignment.create",
|
||||
object_type="view_assignment",
|
||||
object_id=assignment.id,
|
||||
details={
|
||||
"scope_type": assignment.scope_type,
|
||||
"scope_id": assignment.scope_id,
|
||||
"definition_id": assignment.definition_id,
|
||||
"mode": assignment.mode,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return ViewAssignmentResponse.model_validate(assignment_payload(assignment))
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/assignments/{assignment_id}",
|
||||
response_model=ViewAssignmentResponse,
|
||||
)
|
||||
def api_update_assignment(
|
||||
assignment_id: str,
|
||||
payload: ViewAssignmentUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> ViewAssignmentResponse:
|
||||
try:
|
||||
assignment = get_assignment(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
assignment_id=assignment_id,
|
||||
)
|
||||
_require_assignment_write(principal, assignment.scope_type)
|
||||
update_assignment(
|
||||
session,
|
||||
assignment,
|
||||
updates=payload.model_dump(exclude_unset=True),
|
||||
catalogue=_catalogue(),
|
||||
actor_id=_actor_id(principal),
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.assignment.update",
|
||||
object_type="view_assignment",
|
||||
object_id=assignment.id,
|
||||
details={"fields": sorted(payload.model_fields_set)},
|
||||
)
|
||||
session.commit()
|
||||
return ViewAssignmentResponse.model_validate(assignment_payload(assignment))
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/assignments/{assignment_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def api_delete_assignment(
|
||||
assignment_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> None:
|
||||
try:
|
||||
assignment = get_assignment(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
assignment_id=assignment_id,
|
||||
)
|
||||
_require_assignment_write(principal, assignment.scope_type)
|
||||
assignment_details = {
|
||||
"scope_type": assignment.scope_type,
|
||||
"scope_id": assignment.scope_id,
|
||||
"definition_id": assignment.definition_id,
|
||||
"mode": assignment.mode,
|
||||
}
|
||||
delete_assignment(session, assignment)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
action="views.assignment.delete",
|
||||
object_type="view_assignment",
|
||||
object_id=assignment_id,
|
||||
details=assignment_details,
|
||||
)
|
||||
session.commit()
|
||||
except ViewsError as exc:
|
||||
session.rollback()
|
||||
raise _http_error(exc) from exc
|
||||
Reference in New Issue
Block a user