- Added new API endpoints for listing and updating widget-feature assignments, allowing for custom feature requirements. - Introduced a new admin page for managing widget-feature assignments, enhancing the admin interface. - Updated navigation to include a link to the new widget-feature assignments page. - Refactored widget access logic to support AND-based feature requirements for widgets. - Bumped app_dashboard version to 1.11.0 to reflect these changes and improvements.
88 lines
2.8 KiB
Python
88 lines
2.8 KiB
Python
"""
|
||
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
|
||
from widget_feature_requirements_db import get_widget_required_feature_ids
|
||
|
||
|
||
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:
|
||
if _WIDGET_ENTRY_BY_ID.get(widget_id) is None:
|
||
return False
|
||
fids = get_widget_required_feature_ids(widget_id, conn)
|
||
if not fids:
|
||
return True
|
||
for fid in fids:
|
||
if not _check_feature_access(profile_id, fid, conn)["allowed"]:
|
||
return False
|
||
return True
|
||
|
||
|
||
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:
|
||
allowed = widget_id_allowed(e["id"], profile_id, conn)
|
||
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),
|
||
}
|
||
|
||
|
||
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],
|
||
}
|
||
|
||
|
||
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
|
||
|