mitai-jinkendo/backend/dashboard_widget_entitlements.py
Lars 24daeeb83c
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s
feat: Implement widget-feature assignment management in admin dashboard
- 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.
2026-04-08 12:26:28 +02:00

88 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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