Add organization change-control workflow
This commit is contained in:
@@ -8,6 +8,12 @@ from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal, require_any_scope
|
||||
from govoplan_core.core.configuration_control import (
|
||||
ConfigurationChangeApproval,
|
||||
ConfigurationControlError,
|
||||
ensure_configuration_change_allowed,
|
||||
record_configuration_change_applied,
|
||||
)
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunction,
|
||||
@@ -66,6 +72,8 @@ ORG_FUNCTION_WRITE_SCOPES = ("organizations:function:write",)
|
||||
ORG_ASSIGN_SCOPES = ("organizations:function:assign",)
|
||||
ORG_SETTINGS_READ_SCOPES = ("organizations:settings:read", "organizations:model:read", "admin:settings:read")
|
||||
ORG_SETTINGS_WRITE_SCOPES = ("organizations:settings:write", "admin:settings:write")
|
||||
ORG_CHANGE_CONTROL_KEY = "organizations.model"
|
||||
ORG_CHANGE_AUDIT_EVENT = "organizations.model.updated"
|
||||
SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
ModelT = TypeVar("ModelT")
|
||||
@@ -93,6 +101,10 @@ def _invalid(message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=message)
|
||||
|
||||
|
||||
def _configuration_control_http_error(exc: ConfigurationControlError) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail={"code": exc.code, "message": str(exc), "plan": exc.plan})
|
||||
|
||||
|
||||
def _get_tenant_row(session: Session, model: type[ModelT], item_id: str, tenant_id: str, label: str) -> ModelT:
|
||||
item = session.get(model, item_id)
|
||||
if item is None or getattr(item, "tenant_id") != tenant_id:
|
||||
@@ -130,6 +142,83 @@ def _commit(session: Session, item: ModelT) -> ModelT:
|
||||
return item
|
||||
|
||||
|
||||
def _requires_organization_change_request(session: Session, tenant_id: str) -> bool:
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
return bool(item and item.require_model_change_requests)
|
||||
|
||||
|
||||
def _actor_id(principal: ApiPrincipal) -> str:
|
||||
return principal.membership_id or principal.account_id
|
||||
|
||||
|
||||
def _payload_for_control(resource_type: str, operation: str, payload: object) -> dict[str, Any]:
|
||||
if hasattr(payload, "model_dump"):
|
||||
values = payload.model_dump(mode="json", exclude={"change_request_id"}, exclude_unset=True) # type: ignore[attr-defined]
|
||||
else:
|
||||
values = {}
|
||||
return {"resource_type": resource_type, "operation": operation, "payload": values}
|
||||
|
||||
|
||||
def _target_for_control(tenant_id: str, resource_type: str, operation: str, resource_id: str | None = None) -> dict[str, Any]:
|
||||
target: dict[str, Any] = {"tenant_id": tenant_id, "resource_type": resource_type, "operation": operation}
|
||||
if resource_id is not None:
|
||||
target["resource_id"] = resource_id
|
||||
return target
|
||||
|
||||
|
||||
def _ensure_organization_change_allowed(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
tenant_id: str,
|
||||
resource_type: str,
|
||||
operation: str,
|
||||
payload: object,
|
||||
resource_id: str | None = None,
|
||||
) -> tuple[ConfigurationChangeApproval | None, dict[str, Any], dict[str, Any]]:
|
||||
value = _payload_for_control(resource_type, operation, payload)
|
||||
target = _target_for_control(tenant_id, resource_type, operation, resource_id)
|
||||
if not _requires_organization_change_request(session, tenant_id):
|
||||
return None, target, value
|
||||
try:
|
||||
approval = ensure_configuration_change_allowed(
|
||||
session,
|
||||
key=ORG_CHANGE_CONTROL_KEY,
|
||||
value=value,
|
||||
actor_user_id=_actor_id(principal),
|
||||
actor_scopes=tuple(principal.scopes),
|
||||
change_request_id=getattr(payload, "change_request_id", None),
|
||||
target=target,
|
||||
)
|
||||
except ConfigurationControlError as exc:
|
||||
raise _configuration_control_http_error(exc) from exc
|
||||
return approval, target, value
|
||||
|
||||
|
||||
def _record_organization_change_applied(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
*,
|
||||
approval: ConfigurationChangeApproval | None,
|
||||
target: dict[str, Any],
|
||||
before: object,
|
||||
after: object,
|
||||
) -> None:
|
||||
if approval is None:
|
||||
return
|
||||
record_configuration_change_applied(
|
||||
session,
|
||||
key=ORG_CHANGE_CONTROL_KEY,
|
||||
before_value=before,
|
||||
after_value=after,
|
||||
actor_user_id=_actor_id(principal),
|
||||
approval=approval,
|
||||
target=target,
|
||||
audit_event=ORG_CHANGE_AUDIT_EVENT,
|
||||
)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _item_unit_type(item: OrganizationUnitType) -> OrganizationUnitTypeItem:
|
||||
return OrganizationUnitTypeItem(**_row_fields(item))
|
||||
|
||||
@@ -274,11 +363,15 @@ def create_unit_type(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_MODEL_WRITE_SCOPES)),
|
||||
) -> OrganizationUnitTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="unit_type", operation="created", payload=payload)
|
||||
slug = _slug(payload.slug, payload.name)
|
||||
_ensure_unique_slug(session, OrganizationUnitType, tenant_id, slug)
|
||||
item = OrganizationUnitType(tenant_id=tenant_id, slug=slug, name=payload.name.strip(), description=payload.description, is_active=payload.is_active, settings=payload.settings)
|
||||
session.add(item)
|
||||
return _item_unit_type(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_unit_type(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/unit-types/{item_id}", response_model=OrganizationUnitTypeItem)
|
||||
@@ -290,8 +383,12 @@ def update_unit_type(
|
||||
) -> OrganizationUnitTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationUnitType, item_id, tenant_id, "Organization unit type")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="unit_type", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update(session, item, payload, tenant_id, OrganizationUnitType)
|
||||
return _item_unit_type(_commit(session, item))
|
||||
result = _item_unit_type(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/structures", response_model=OrganizationStructureItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -301,11 +398,15 @@ def create_structure(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_MODEL_WRITE_SCOPES)),
|
||||
) -> OrganizationStructureItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="structure", operation="created", payload=payload)
|
||||
slug = _slug(payload.slug, payload.name)
|
||||
_ensure_unique_slug(session, OrganizationStructure, tenant_id, slug)
|
||||
item = OrganizationStructure(tenant_id=tenant_id, slug=slug, name=payload.name.strip(), description=payload.description, structure_kind=payload.structure_kind, is_active=payload.is_active, settings=payload.settings)
|
||||
session.add(item)
|
||||
return _item_structure(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_structure(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/structures/{item_id}", response_model=OrganizationStructureItem)
|
||||
@@ -317,12 +418,16 @@ def update_structure(
|
||||
) -> OrganizationStructureItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationStructure, item_id, tenant_id, "Organization structure")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="structure", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update(session, item, payload, tenant_id, OrganizationStructure)
|
||||
if "structure_kind" in payload.model_fields_set:
|
||||
if payload.structure_kind is None:
|
||||
raise _invalid("Structure kind cannot be empty.")
|
||||
item.structure_kind = payload.structure_kind
|
||||
return _item_structure(_commit(session, item))
|
||||
result = _item_structure(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/relation-types", response_model=OrganizationRelationTypeItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -332,6 +437,7 @@ def create_relation_type(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_MODEL_WRITE_SCOPES)),
|
||||
) -> OrganizationRelationTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="relation_type", operation="created", payload=payload)
|
||||
_ensure_optional_structure(session, tenant_id, payload.structure_id)
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.source_unit_type_id)
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.target_unit_type_id)
|
||||
@@ -351,7 +457,10 @@ def create_relation_type(
|
||||
settings=payload.settings,
|
||||
)
|
||||
session.add(item)
|
||||
return _item_relation_type(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_relation_type(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/relation-types/{item_id}", response_model=OrganizationRelationTypeItem)
|
||||
@@ -363,6 +472,8 @@ def update_relation_type(
|
||||
) -> OrganizationRelationTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationRelationType, item_id, tenant_id, "Organization relation type")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="relation_type", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update(session, item, payload, tenant_id, OrganizationRelationType)
|
||||
fields = payload.model_fields_set
|
||||
if "structure_id" in fields:
|
||||
@@ -380,7 +491,9 @@ def update_relation_type(
|
||||
if value is None:
|
||||
raise _invalid(f"{field} cannot be empty.")
|
||||
setattr(item, field, value)
|
||||
return _item_relation_type(_commit(session, item))
|
||||
result = _item_relation_type(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/units", response_model=OrganizationUnitItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -390,6 +503,7 @@ def create_unit(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_UNIT_WRITE_SCOPES)),
|
||||
) -> OrganizationUnitItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="unit", operation="created", payload=payload)
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.unit_type_id)
|
||||
if payload.parent_id is not None:
|
||||
_get_tenant_row(session, OrganizationUnit, payload.parent_id, tenant_id, "Parent organization unit")
|
||||
@@ -397,7 +511,10 @@ def create_unit(
|
||||
_ensure_unique_slug(session, OrganizationUnit, tenant_id, slug)
|
||||
item = OrganizationUnit(tenant_id=tenant_id, unit_type_id=payload.unit_type_id, parent_id=payload.parent_id, slug=slug, name=payload.name.strip(), description=payload.description, is_active=payload.is_active, settings=payload.settings)
|
||||
session.add(item)
|
||||
return _item_unit(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_unit(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/units/{item_id}", response_model=OrganizationUnitItem)
|
||||
@@ -409,6 +526,8 @@ def update_unit(
|
||||
) -> OrganizationUnitItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationUnit, item_id, tenant_id, "Organization unit")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="unit", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update(session, item, payload, tenant_id, OrganizationUnit)
|
||||
if "unit_type_id" in payload.model_fields_set:
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.unit_type_id)
|
||||
@@ -421,7 +540,9 @@ def update_unit(
|
||||
if _would_create_parent_cycle(session, tenant_id, item.id, payload.parent_id):
|
||||
raise _invalid("This parent assignment would create an organization unit cycle.")
|
||||
item.parent_id = payload.parent_id
|
||||
return _item_unit(_commit(session, item))
|
||||
result = _item_unit(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/relations", response_model=OrganizationRelationItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -431,7 +552,8 @@ def create_relation(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_UNIT_WRITE_SCOPES)),
|
||||
) -> OrganizationRelationItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
structure, relation_type, source, target = _validated_relation_parts(
|
||||
approval, control_target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="relation", operation="created", payload=payload)
|
||||
structure, relation_type, source, target_unit = _validated_relation_parts(
|
||||
session,
|
||||
tenant_id,
|
||||
structure_id=payload.structure_id,
|
||||
@@ -439,20 +561,23 @@ def create_relation(
|
||||
source_unit_id=payload.source_unit_id,
|
||||
target_unit_id=payload.target_unit_id,
|
||||
)
|
||||
_validate_relation_edge(session, tenant_id, structure, relation_type, source, target)
|
||||
_validate_relation_edge(session, tenant_id, structure, relation_type, source, target_unit)
|
||||
item = OrganizationRelation(
|
||||
tenant_id=tenant_id,
|
||||
structure_id=structure.id,
|
||||
relation_type_id=relation_type.id,
|
||||
source_unit_id=source.id,
|
||||
target_unit_id=target.id,
|
||||
target_unit_id=target_unit.id,
|
||||
valid_from=payload.valid_from,
|
||||
valid_until=payload.valid_until,
|
||||
is_active=payload.is_active,
|
||||
settings=payload.settings,
|
||||
)
|
||||
session.add(item)
|
||||
return _item_relation(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_relation(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**control_target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/relations/{item_id}", response_model=OrganizationRelationItem)
|
||||
@@ -464,13 +589,15 @@ def update_relation(
|
||||
) -> OrganizationRelationItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationRelation, item_id, tenant_id, "Organization relation")
|
||||
before = _row_fields(item)
|
||||
approval, control_target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="relation", operation="updated", payload=payload, resource_id=item_id)
|
||||
structure_id = payload.structure_id if "structure_id" in payload.model_fields_set else item.structure_id
|
||||
relation_type_id = payload.relation_type_id if "relation_type_id" in payload.model_fields_set else item.relation_type_id
|
||||
source_unit_id = payload.source_unit_id if "source_unit_id" in payload.model_fields_set else item.source_unit_id
|
||||
target_unit_id = payload.target_unit_id if "target_unit_id" in payload.model_fields_set else item.target_unit_id
|
||||
if structure_id is None or relation_type_id is None or source_unit_id is None or target_unit_id is None:
|
||||
raise _invalid("Relation structure, type, source, and target are required.")
|
||||
structure, relation_type, source, target = _validated_relation_parts(
|
||||
structure, relation_type, source, target_unit = _validated_relation_parts(
|
||||
session,
|
||||
tenant_id,
|
||||
structure_id=structure_id,
|
||||
@@ -478,15 +605,17 @@ def update_relation(
|
||||
source_unit_id=source_unit_id,
|
||||
target_unit_id=target_unit_id,
|
||||
)
|
||||
_validate_relation_edge(session, tenant_id, structure, relation_type, source, target, exclude_relation_id=item.id)
|
||||
_validate_relation_edge(session, tenant_id, structure, relation_type, source, target_unit, exclude_relation_id=item.id)
|
||||
item.structure_id = structure.id
|
||||
item.relation_type_id = relation_type.id
|
||||
item.source_unit_id = source.id
|
||||
item.target_unit_id = target.id
|
||||
item.target_unit_id = target_unit.id
|
||||
for field in ("valid_from", "valid_until", "is_active", "settings"):
|
||||
if field in payload.model_fields_set:
|
||||
setattr(item, field, getattr(payload, field))
|
||||
return _item_relation(_commit(session, item))
|
||||
result = _item_relation(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=control_target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/function-types", response_model=OrganizationFunctionTypeItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -496,6 +625,7 @@ def create_function_type(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_MODEL_WRITE_SCOPES)),
|
||||
) -> OrganizationFunctionTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_type", operation="created", payload=payload)
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.organization_unit_type_id)
|
||||
slug = _slug(payload.slug, payload.name)
|
||||
_ensure_unique_slug(session, OrganizationFunctionType, tenant_id, slug)
|
||||
@@ -511,7 +641,10 @@ def create_function_type(
|
||||
settings=payload.settings,
|
||||
)
|
||||
session.add(item)
|
||||
return _item_function_type(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_function_type(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/function-types/{item_id}", response_model=OrganizationFunctionTypeItem)
|
||||
@@ -523,6 +656,8 @@ def update_function_type(
|
||||
) -> OrganizationFunctionTypeItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationFunctionType, item_id, tenant_id, "Organization function type")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_type", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update(session, item, payload, tenant_id, OrganizationFunctionType)
|
||||
if "organization_unit_type_id" in payload.model_fields_set:
|
||||
_ensure_optional_unit_type(session, tenant_id, payload.organization_unit_type_id)
|
||||
@@ -533,7 +668,9 @@ def update_function_type(
|
||||
if value is None:
|
||||
raise _invalid(f"{field} cannot be empty.")
|
||||
setattr(item, field, value)
|
||||
return _item_function_type(_commit(session, item))
|
||||
result = _item_function_type(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/functions", response_model=OrganizationFunctionItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -543,6 +680,7 @@ def create_function(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_FUNCTION_WRITE_SCOPES)),
|
||||
) -> OrganizationFunctionItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function", operation="created", payload=payload)
|
||||
unit = _get_tenant_row(session, OrganizationUnit, payload.organization_unit_id, tenant_id, "Organization unit")
|
||||
function_type = _optional_function_type(session, tenant_id, payload.function_type_id)
|
||||
slug = _slug(payload.slug, payload.name)
|
||||
@@ -562,7 +700,10 @@ def create_function(
|
||||
settings=payload.settings,
|
||||
)
|
||||
session.add(item)
|
||||
return _item_function(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_function(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/functions/{item_id}", response_model=OrganizationFunctionItem)
|
||||
@@ -574,6 +715,8 @@ def update_function(
|
||||
) -> OrganizationFunctionItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationFunction, item_id, tenant_id, "Organization function")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function", operation="updated", payload=payload, resource_id=item_id)
|
||||
_apply_slugged_update_for_function(session, item, payload, tenant_id)
|
||||
if "organization_unit_id" in payload.model_fields_set:
|
||||
if payload.organization_unit_id is None:
|
||||
@@ -590,7 +733,9 @@ def update_function(
|
||||
if value is None:
|
||||
raise _invalid(f"{field} cannot be empty.")
|
||||
setattr(item, field, value)
|
||||
return _item_function(_commit(session, item))
|
||||
result = _item_function(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/function-assignments", response_model=OrganizationFunctionAssignmentItem, status_code=status.HTTP_201_CREATED)
|
||||
@@ -600,6 +745,7 @@ def create_function_assignment(
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_ASSIGN_SCOPES)),
|
||||
) -> OrganizationFunctionAssignmentItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_assignment", operation="created", payload=payload)
|
||||
function = _get_tenant_row(session, OrganizationFunction, payload.function_id, tenant_id, "Organization function")
|
||||
item = OrganizationFunctionAssignment(
|
||||
tenant_id=tenant_id,
|
||||
@@ -619,7 +765,10 @@ def create_function_assignment(
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
_get_tenant_row(session, OrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
session.add(item)
|
||||
return _item_assignment(_commit(session, item))
|
||||
saved = _commit(session, item)
|
||||
result = _item_assignment(saved)
|
||||
_record_organization_change_applied(session, principal, approval=approval, target={**target, "resource_id": saved.id}, before=None, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/function-assignments/{item_id}", response_model=OrganizationFunctionAssignmentItem)
|
||||
@@ -631,6 +780,8 @@ def update_function_assignment(
|
||||
) -> OrganizationFunctionAssignmentItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = _get_tenant_row(session, OrganizationFunctionAssignment, item_id, tenant_id, "Organization function assignment")
|
||||
before = _row_fields(item)
|
||||
approval, target, _value = _ensure_organization_change_allowed(session, principal, tenant_id=tenant_id, resource_type="function_assignment", operation="updated", payload=payload, resource_id=item_id)
|
||||
if "function_id" in payload.model_fields_set:
|
||||
if payload.function_id is None:
|
||||
raise _invalid("Function is required.")
|
||||
@@ -665,7 +816,9 @@ def update_function_assignment(
|
||||
raise _invalid("A function assignment cannot delegate from itself.")
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
_get_tenant_row(session, OrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
return _item_assignment(_commit(session, item))
|
||||
result = _item_assignment(_commit(session, item))
|
||||
_record_organization_change_applied(session, principal, approval=approval, target=target, before=before, after=result.model_dump(mode="json"))
|
||||
return result
|
||||
|
||||
|
||||
def _optional_function_type(session: Session, tenant_id: str, function_type_id: str | None) -> OrganizationFunctionType | None:
|
||||
|
||||
@@ -168,6 +168,7 @@ class SluggedCreateRequest(BaseModel):
|
||||
description: str | None = None
|
||||
is_active: bool = True
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class SluggedUpdateRequest(BaseModel):
|
||||
@@ -176,6 +177,7 @@ class SluggedUpdateRequest(BaseModel):
|
||||
description: str | None = None
|
||||
is_active: bool | None = None
|
||||
settings: dict[str, Any] | None = None
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class UnitTypeCreateRequest(SluggedCreateRequest):
|
||||
@@ -229,6 +231,7 @@ class RelationCreateRequest(BaseModel):
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool = True
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class RelationUpdateRequest(BaseModel):
|
||||
@@ -240,6 +243,7 @@ class RelationUpdateRequest(BaseModel):
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool | None = None
|
||||
settings: dict[str, Any] | None = None
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class FunctionTypeCreateRequest(SluggedCreateRequest):
|
||||
@@ -280,6 +284,7 @@ class FunctionAssignmentCreateRequest(BaseModel):
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool = True
|
||||
settings: dict[str, Any] = Field(default_factory=dict)
|
||||
change_request_id: str | None = None
|
||||
|
||||
|
||||
class FunctionAssignmentUpdateRequest(BaseModel):
|
||||
@@ -294,3 +299,4 @@ class FunctionAssignmentUpdateRequest(BaseModel):
|
||||
valid_until: datetime | None = None
|
||||
is_active: bool | None = None
|
||||
settings: dict[str, Any] | None = None
|
||||
change_request_id: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user