feat(quick-access): expose effective preference provenance
This commit is contained in:
@@ -174,6 +174,8 @@ DOCUMENTATION = (
|
||||
body=(
|
||||
"The catalogue follows installed module registrations. System settings constrain tenants; "
|
||||
"tenant settings constrain users. An item may remain available, be blocked, or be forced. "
|
||||
"Effective entries identify the system, tenant, user, or module source of availability and ordering; "
|
||||
"preferences for retired entries retain their scope provenance. "
|
||||
"Views and permissions form additional ceilings and Quick Access never grants access to domain data. "
|
||||
"A View may recommend tools or focus the rail to a subset, but only currently enabled, context-compatible, authorized "
|
||||
"tools participate. The All available tools escape only restores that permission-derived set for the current session; "
|
||||
@@ -191,6 +193,8 @@ DOCUMENTATION = (
|
||||
"body": (
|
||||
"Der Katalog folgt den Registrierungen installierter Module. Systemeinstellungen begrenzen Mandanten, "
|
||||
"Mandanteneinstellungen begrenzen Benutzer. Ein Eintrag kann verfuegbar, gesperrt oder erzwungen sein. "
|
||||
"Effektive Eintraege nennen System, Mandant, Benutzer oder Modul als Quelle fuer Verfuegbarkeit und Reihenfolge; "
|
||||
"Einstellungen fuer entfernte Eintraege behalten ihren Ebenennachweis. "
|
||||
"Ansichten und Berechtigungen bilden weitere Grenzen; Schnellzugriff erteilt selbst keinen Datenzugriff. "
|
||||
"Eine Ansicht darf Werkzeuge empfehlen oder die Leiste auf eine Teilmenge fokussieren, jedoch nur innerhalb "
|
||||
"der aktivierten, kontextgeeigneten und berechtigten Werkzeuge. Alle verfuegbaren Werkzeuge stellt nur diese "
|
||||
|
||||
@@ -71,17 +71,30 @@ class EffectiveToolResponse(CatalogueToolResponse):
|
||||
enabled: bool
|
||||
forced: bool
|
||||
locked_by: str | None = None
|
||||
availability_state: Literal["available", "forced", "blocked"]
|
||||
availability_source: Literal["module", "system", "tenant", "user"]
|
||||
order_source: Literal["module", "system", "tenant", "user"]
|
||||
|
||||
|
||||
class EffectiveCategoryResponse(CatalogueCategoryResponse):
|
||||
enabled: bool
|
||||
forced: bool
|
||||
locked_by: str | None = None
|
||||
availability_state: Literal["available", "forced", "blocked"]
|
||||
availability_source: Literal["module", "system", "tenant", "user"]
|
||||
order_source: Literal["module", "system", "tenant", "user"]
|
||||
tools: list[EffectiveToolResponse]
|
||||
|
||||
|
||||
class EffectiveStalePreferenceResponse(BaseModel):
|
||||
id: str
|
||||
kind: Literal["category", "tool"]
|
||||
source: Literal["system", "tenant", "user"]
|
||||
|
||||
|
||||
class EffectiveQuickAccessResponse(BaseModel):
|
||||
categories: list[EffectiveCategoryResponse]
|
||||
stale_preferences: list[EffectiveStalePreferenceResponse] = Field(default_factory=list)
|
||||
diagnostics: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from govoplan_quick_access.backend.schemas import (
|
||||
CatalogueToolResponse,
|
||||
EffectiveCategoryResponse,
|
||||
EffectiveQuickAccessResponse,
|
||||
EffectiveStalePreferenceResponse,
|
||||
EffectiveToolResponse,
|
||||
PreferenceEntry,
|
||||
ProfileResponse,
|
||||
@@ -241,6 +242,7 @@ def resolve_effective(
|
||||
("user", get_profile(session, scope_type="user", tenant_id=tenant_id, scope_id=account_id)),
|
||||
)
|
||||
diagnostics: list[str] = []
|
||||
stale_preferences = _stale_preferences(profiles, catalogue)
|
||||
category_states: dict[str, _EffectiveState] = {}
|
||||
for category in catalogue.categories:
|
||||
state = _EffectiveState(enabled=True, order=category.order)
|
||||
@@ -259,6 +261,11 @@ def resolve_effective(
|
||||
state.apply(entry, source=source)
|
||||
category_state = category_states.get(tool.category_id)
|
||||
enabled = state.enabled and bool(category_state and category_state.enabled)
|
||||
availability_source = (
|
||||
category_state.availability_source
|
||||
if state.enabled and category_state and not category_state.enabled
|
||||
else state.availability_source
|
||||
)
|
||||
tool_payload = tool.model_dump()
|
||||
tool_payload["order"] = state.order
|
||||
tools_by_category.setdefault(tool.category_id, []).append(
|
||||
@@ -267,6 +274,9 @@ def resolve_effective(
|
||||
enabled=enabled,
|
||||
forced=state.forced,
|
||||
locked_by=state.locked_by,
|
||||
availability_state=_availability_state(enabled, state.forced),
|
||||
availability_source=availability_source,
|
||||
order_source=state.order_source,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -286,12 +296,16 @@ def resolve_effective(
|
||||
enabled=enabled,
|
||||
forced=state.forced,
|
||||
locked_by=state.locked_by,
|
||||
availability_state=_availability_state(enabled, state.forced),
|
||||
availability_source=state.availability_source,
|
||||
order_source=state.order_source,
|
||||
tools=tools,
|
||||
)
|
||||
)
|
||||
categories.sort(key=lambda item: (item.order, item.id))
|
||||
return EffectiveQuickAccessResponse(
|
||||
categories=categories,
|
||||
stale_preferences=stale_preferences,
|
||||
diagnostics=list(dict.fromkeys(diagnostics)),
|
||||
)
|
||||
|
||||
@@ -302,16 +316,20 @@ class _EffectiveState:
|
||||
order: int
|
||||
forced: bool = False
|
||||
locked_by: str | None = None
|
||||
availability_source: str = "module"
|
||||
order_source: str = "module"
|
||||
|
||||
def apply(self, entry: PreferenceEntry | None, *, source: str) -> None:
|
||||
if entry is None:
|
||||
return
|
||||
if entry.order is not None:
|
||||
self.order = entry.order
|
||||
self.order_source = source
|
||||
if self.locked_by is not None:
|
||||
return
|
||||
if entry.enabled is not None:
|
||||
self.enabled = entry.enabled
|
||||
self.availability_source = source
|
||||
if source != "user" and entry.enabled is False:
|
||||
self.forced = False
|
||||
self.locked_by = source
|
||||
@@ -319,6 +337,38 @@ class _EffectiveState:
|
||||
self.enabled = True
|
||||
self.forced = True
|
||||
self.locked_by = source
|
||||
self.availability_source = source
|
||||
|
||||
|
||||
def _availability_state(enabled: bool, forced: bool) -> str:
|
||||
if not enabled:
|
||||
return "blocked"
|
||||
return "forced" if forced else "available"
|
||||
|
||||
|
||||
def _stale_preferences(
|
||||
profiles: tuple[tuple[str, QuickAccessProfile | None], ...],
|
||||
catalogue: CatalogueResponse,
|
||||
) -> list[EffectiveStalePreferenceResponse]:
|
||||
category_ids = {item.id for item in catalogue.categories}
|
||||
tool_ids = {item.id for item in catalogue.tools}
|
||||
stale: list[EffectiveStalePreferenceResponse] = []
|
||||
for source, profile in profiles:
|
||||
if profile is None:
|
||||
continue
|
||||
for kind, field, known_ids in (
|
||||
("category", "category_preferences", category_ids),
|
||||
("tool", "tool_preferences", tool_ids),
|
||||
):
|
||||
values = getattr(profile, field, {})
|
||||
if not isinstance(values, Mapping):
|
||||
continue
|
||||
stale.extend(
|
||||
EffectiveStalePreferenceResponse(id=str(item_id), kind=kind, source=source)
|
||||
for item_id in values
|
||||
if str(item_id) not in known_ids
|
||||
)
|
||||
return sorted(stale, key=lambda item: (item.source, item.kind, item.id))
|
||||
|
||||
|
||||
def _profile_entry(
|
||||
|
||||
Reference in New Issue
Block a user