feat: manage custom appearance policy

This commit is contained in:
2026-08-20 10:50:55 +02:00
parent 50c952565b
commit 935cee8ccd
7 changed files with 49 additions and 7 deletions
+24 -2
View File
@@ -15,7 +15,12 @@ from govoplan_core.audit.logging import audit_from_principal, audit_operation_co
from govoplan_admin.backend.db.models import GovernanceTemplate, GovernanceTemplateAssignment
from govoplan_core.admin.common import AdminConflictError, AdminValidationError
from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration
from govoplan_core.core.appearance import appearance_settings, update_appearance_settings
from govoplan_core.core.appearance import (
appearance_custom_overrides_policy,
appearance_settings,
update_appearance_custom_overrides_policy,
update_appearance_settings,
)
from govoplan_core.core.change_sequence import (
decode_sequence_watermark,
encode_sequence_watermark,
@@ -311,6 +316,7 @@ def _system_settings_item(session: Session) -> SystemSettingsItem:
navigation=navigation.as_dict() if navigation is not None else None,
appearance_palette=appearance_palette or "default",
appearance_palette_locked=appearance_palette_locked,
appearance_custom_overrides_allowed=appearance_custom_overrides_policy(item.settings) is True,
)
@@ -333,6 +339,7 @@ def _system_settings_sections(item: SystemSettingsItem) -> dict[str, Any]:
"appearance": {
"appearance_palette": payload["appearance_palette"],
"appearance_palette_locked": payload["appearance_palette_locked"],
"appearance_custom_overrides_allowed": payload["appearance_custom_overrides_allowed"],
},
"settings": payload["settings"],
}
@@ -1793,21 +1800,28 @@ def write_system_settings(
before_privacy = privacy_policy_from_settings(item).model_dump(mode="json")
before_maintenance = saved_maintenance_mode(session).as_dict()
before_appearance_palette, before_appearance_locked = appearance_settings(item.settings)
before_custom_overrides_allowed = appearance_custom_overrides_policy(item.settings) is True
appearance_palette_changed = (
"appearance_palette" in payload.model_fields_set
and payload.appearance_palette is not None
and payload.appearance_palette != (before_appearance_palette or "default")
)
custom_overrides_policy_changed = (
"appearance_custom_overrides_allowed" in payload.model_fields_set
and payload.appearance_custom_overrides_allowed is not None
and payload.appearance_custom_overrides_allowed != before_custom_overrides_allowed
)
if (
(
"appearance_palette_locked" in payload.model_fields_set
and payload.appearance_palette_locked != before_appearance_locked
)
or (before_appearance_locked and appearance_palette_changed)
or custom_overrides_policy_changed
) and not has_scope(principal, "admin:policies:write"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="Changing the system appearance lock or its locked value requires admin:policies:write.",
detail="Changing the system appearance policy requires admin:policies:write.",
)
if payload.privacy_retention_policy is not None:
privacy_value = payload.privacy_retention_policy.model_dump(mode="json")
@@ -1852,6 +1866,14 @@ def write_system_settings(
default_palette=payload.appearance_palette or current_palette or "default",
palette_locked=payload.appearance_palette_locked if payload.appearance_palette_locked is not None else current_locked,
)
if (
"appearance_custom_overrides_allowed" in payload.model_fields_set
and payload.appearance_custom_overrides_allowed is not None
):
item.settings = update_appearance_custom_overrides_policy(
item.settings,
allowed=payload.appearance_custom_overrides_allowed,
)
if "navigation" in payload.model_fields_set:
item.settings = update_navigation_preferences(
item.settings,
@@ -53,6 +53,7 @@ class SystemSettingsItem(BaseModel):
navigation: NavigationPreferencesPayload | None = None
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default"
appearance_palette_locked: bool = False
appearance_custom_overrides_allowed: bool = False
class SystemSettingsDeltaResponse(BaseModel):
@@ -79,6 +80,7 @@ class SystemSettingsUpdateRequest(BaseModel):
navigation: NavigationPreferencesPayload | None = None
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None
appearance_palette_locked: bool | None = None
appearance_custom_overrides_allowed: bool | None = None
change_request_id: str | None = None
+1 -1
View File
@@ -75,7 +75,7 @@ manifest = ModuleManifest(
id="admin.workspace",
title="Use the administration workspace",
summary="The administration workspace shows only the sections supplied by enabled modules and allowed by the current account's permissions.",
body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable.",
body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable. The separate advanced-override policy is disabled by default and also requires policy-write authority. Enabling it permits tenants to inherit or narrow access to Core's versioned, accessibility-validated personal accent, surface, and status editor; palette locks continue to win.",
documentation_types=("admin", "user"),
audience=("tenant_admin", "system_admin", "operator"),
metadata={