2026-04-08 07:21:49 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Dashboard-Widgets × Feature-System: Sichtbarkeit aus check_feature_access.
|
|
|
|
|
|
|
|
|
|
|
|
Katalog-Einträge optional `requires_feature` (features.id). Fehlt der Key → immer erlaubt.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import copy
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from widget_catalog import WIDGET_CATALOG
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_feature_access(profile_id: str, feature_id: str, conn) -> dict:
|
|
|
|
|
|
"""Indirection für Tests (monkeypatch) und spätes Laden von auth (bcrypt)."""
|
|
|
|
|
|
from auth import check_feature_access
|
|
|
|
|
|
|
|
|
|
|
|
return check_feature_access(profile_id, feature_id, conn)
|
|
|
|
|
|
|
|
|
|
|
|
_WIDGET_ENTRY_BY_ID: dict[str, dict[str, Any]] = {e["id"]: e for e in WIDGET_CATALOG}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def widget_id_allowed(widget_id: str, profile_id: str, conn) -> bool:
|
|
|
|
|
|
entry = _WIDGET_ENTRY_BY_ID.get(widget_id)
|
|
|
|
|
|
if entry is None:
|
|
|
|
|
|
return False
|
|
|
|
|
|
fid = entry.get("requires_feature")
|
|
|
|
|
|
if not fid:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return bool(_check_feature_access(profile_id, fid, conn)["allowed"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _public_row(entry: dict[str, Any], *, allowed: bool) -> dict[str, Any]:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": entry["id"],
|
|
|
|
|
|
"title": entry["title"],
|
|
|
|
|
|
"description": entry["description"],
|
|
|
|
|
|
"allowed": allowed,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def widgets_catalog_for_profile(profile_id: str, conn) -> list[dict[str, Any]]:
|
|
|
|
|
|
"""Zeilen für GET /api/app/widgets/catalog (ohne internes requires_feature-Feld)."""
|
|
|
|
|
|
out: list[dict[str, Any]] = []
|
|
|
|
|
|
for e in WIDGET_CATALOG:
|
|
|
|
|
|
fid = e.get("requires_feature")
|
|
|
|
|
|
allowed = True
|
|
|
|
|
|
if fid:
|
|
|
|
|
|
allowed = bool(_check_feature_access(profile_id, fid, conn)["allowed"])
|
|
|
|
|
|
out.append(_public_row(e, allowed=allowed))
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def widgets_catalog_payload(profile_id: str, conn) -> dict[str, Any]:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"catalog_version": 1,
|
|
|
|
|
|
"widgets": widgets_catalog_for_profile(profile_id, conn),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 10:32:18 +02:00
|
|
|
|
def widgets_catalog_admin_payload() -> dict[str, Any]:
|
|
|
|
|
|
"""Admin: alle Widgets als auswählbar (ohne Feature-Filter)."""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"catalog_version": 1,
|
|
|
|
|
|
"widgets": [_public_row(e, allowed=True) for e in WIDGET_CATALOG],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 07:21:49 +02:00
|
|
|
|
def apply_entitlements_to_layout_dict(layout: dict[str, Any], profile_id: str, conn) -> dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Setzt enabled=False für Widgets ohne Berechtigung. Mindestens ein Widget bleibt aktiv (welcome).
|
|
|
|
|
|
"""
|
|
|
|
|
|
out = copy.deepcopy(layout)
|
|
|
|
|
|
widgets = out.get("widgets") or []
|
|
|
|
|
|
for w in widgets:
|
|
|
|
|
|
wid = w.get("id")
|
|
|
|
|
|
if not wid:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if w.get("enabled") and not widget_id_allowed(wid, profile_id, conn):
|
|
|
|
|
|
w["enabled"] = False
|
|
|
|
|
|
if not any(w.get("enabled") for w in widgets):
|
|
|
|
|
|
for w in widgets:
|
|
|
|
|
|
if w.get("id") == "welcome":
|
|
|
|
|
|
w["enabled"] = True
|
|
|
|
|
|
break
|
|
|
|
|
|
return out
|
|
|
|
|
|
|