Add organization tenant settings
This commit is contained in:
@@ -15,6 +15,7 @@ from govoplan_organizations.backend.db.models import (
|
||||
OrganizationFunctionType,
|
||||
OrganizationRelation,
|
||||
OrganizationRelationType,
|
||||
OrganizationTenantSettings,
|
||||
OrganizationStructure,
|
||||
OrganizationUnit,
|
||||
OrganizationUnitType,
|
||||
@@ -33,6 +34,8 @@ from .schemas import (
|
||||
OrganizationModelResponse,
|
||||
OrganizationRelationItem,
|
||||
OrganizationRelationTypeItem,
|
||||
OrganizationSettingsItem,
|
||||
OrganizationSettingsUpdateRequest,
|
||||
OrganizationStructureItem,
|
||||
OrganizationUnitItem,
|
||||
OrganizationUnitTypeItem,
|
||||
@@ -61,6 +64,8 @@ ORG_MODEL_WRITE_SCOPES = ("organizations:model:write",)
|
||||
ORG_UNIT_WRITE_SCOPES = ("organizations:unit:write",)
|
||||
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")
|
||||
SLUG_RE = re.compile(r"[^a-z0-9]+")
|
||||
|
||||
ModelT = TypeVar("ModelT")
|
||||
@@ -129,6 +134,21 @@ def _item_unit_type(item: OrganizationUnitType) -> OrganizationUnitTypeItem:
|
||||
return OrganizationUnitTypeItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _item_settings(item: OrganizationTenantSettings) -> OrganizationSettingsItem:
|
||||
return OrganizationSettingsItem(**_row_fields(item))
|
||||
|
||||
|
||||
def _default_settings(tenant_id: str) -> OrganizationSettingsItem:
|
||||
return OrganizationSettingsItem(
|
||||
tenant_id=tenant_id,
|
||||
allow_tenant_model_customization=True,
|
||||
require_model_change_requests=False,
|
||||
audit_detail_level="standard",
|
||||
change_retention_days=None,
|
||||
settings={},
|
||||
)
|
||||
|
||||
|
||||
def _item_structure(item: OrganizationStructure) -> OrganizationStructureItem:
|
||||
return OrganizationStructureItem(**_row_fields(item))
|
||||
|
||||
@@ -187,6 +207,48 @@ def _apply_slugged_update(session: Session, item: object, payload: object, tenan
|
||||
setattr(item, "settings", value)
|
||||
|
||||
|
||||
@router.get("/settings", response_model=OrganizationSettingsItem)
|
||||
def get_organization_settings(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_READ_SCOPES)),
|
||||
) -> OrganizationSettingsItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
return _item_settings(item) if item is not None else _default_settings(tenant_id)
|
||||
|
||||
|
||||
@router.patch("/settings", response_model=OrganizationSettingsItem)
|
||||
def update_organization_settings(
|
||||
payload: OrganizationSettingsUpdateRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORG_SETTINGS_WRITE_SCOPES)),
|
||||
) -> OrganizationSettingsItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
item = session.query(OrganizationTenantSettings).filter(OrganizationTenantSettings.tenant_id == tenant_id).one_or_none()
|
||||
if item is None:
|
||||
item = OrganizationTenantSettings(
|
||||
tenant_id=tenant_id,
|
||||
allow_tenant_model_customization=True,
|
||||
require_model_change_requests=False,
|
||||
audit_detail_level="standard",
|
||||
change_retention_days=None,
|
||||
settings={},
|
||||
)
|
||||
session.add(item)
|
||||
fields = payload.model_fields_set
|
||||
for field in ("allow_tenant_model_customization", "require_model_change_requests", "audit_detail_level", "change_retention_days"):
|
||||
if field in fields:
|
||||
value = getattr(payload, field)
|
||||
if field != "change_retention_days" and value is None:
|
||||
raise _invalid(f"{field} cannot be empty.")
|
||||
setattr(item, field, value)
|
||||
if "settings" in fields:
|
||||
if payload.settings is None:
|
||||
raise _invalid("Settings cannot be empty.")
|
||||
item.settings = payload.settings
|
||||
return _item_settings(_commit(session, item))
|
||||
|
||||
|
||||
@router.get("/model", response_model=OrganizationModelResponse)
|
||||
def get_organization_model(
|
||||
session: Session = Depends(get_session),
|
||||
@@ -356,6 +418,8 @@ def update_unit(
|
||||
raise _invalid("An organization unit cannot be its own parent.")
|
||||
if payload.parent_id is not None:
|
||||
_get_tenant_row(session, OrganizationUnit, payload.parent_id, tenant_id, "Parent organization 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))
|
||||
|
||||
@@ -718,3 +782,17 @@ def _would_create_cycle(
|
||||
query = query.filter(OrganizationRelation.id != exclude_relation_id)
|
||||
pending.extend(row[0] for row in query.all())
|
||||
return False
|
||||
|
||||
|
||||
def _would_create_parent_cycle(session: Session, tenant_id: str, unit_id: str, parent_id: str) -> bool:
|
||||
current: str | None = parent_id
|
||||
seen: set[str] = set()
|
||||
while current is not None:
|
||||
if current == unit_id or current in seen:
|
||||
return True
|
||||
seen.add(current)
|
||||
current = session.query(OrganizationUnit.parent_id).filter(
|
||||
OrganizationUnit.tenant_id == tenant_id,
|
||||
OrganizationUnit.id == current,
|
||||
).scalar()
|
||||
return False
|
||||
|
||||
@@ -7,6 +7,27 @@ from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
StructureKind = Literal["hierarchy", "network", "membership", "classification"]
|
||||
AuditDetailLevel = Literal["summary", "standard", "full"]
|
||||
|
||||
|
||||
class OrganizationSettingsItem(BaseModel):
|
||||
id: str | None = None
|
||||
tenant_id: str
|
||||
allow_tenant_model_customization: bool
|
||||
require_model_change_requests: bool
|
||||
audit_detail_level: AuditDetailLevel
|
||||
change_retention_days: int | None = None
|
||||
settings: dict[str, Any]
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
class OrganizationSettingsUpdateRequest(BaseModel):
|
||||
allow_tenant_model_customization: bool | None = None
|
||||
require_model_change_requests: bool | None = None
|
||||
audit_detail_level: AuditDetailLevel | None = None
|
||||
change_retention_days: int | None = Field(default=None, ge=0)
|
||||
settings: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class OrganizationUnitTypeItem(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user