- Updated dashboard layout schema to introduce separate default layouts for product and lab dashboards. - Added new functions for managing product and lab default layouts, improving user customization options. - Updated app_dashboard version to 1.9.0 to reflect the introduction of product vs lab layout defaults and new API fields for dashboard configuration. - Enhanced tests to validate new layout functionalities and ensure proper widget visibility based on user settings.
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""
|
|
Geschützter App-Bereich: Dashboard-Lab Layout (kein Produktiv-Dashboard).
|
|
|
|
/api/app/dashboard-layout — nur mit Session + aktivem Profil (X-Profile-Id).
|
|
"""
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
from psycopg2.extras import Json
|
|
|
|
from auth import require_auth
|
|
from dashboard_layout_schema import (
|
|
DashboardLayoutPayload,
|
|
coalesce_effective_layout,
|
|
lab_default_layout_dict,
|
|
product_default_layout_dict,
|
|
)
|
|
from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload
|
|
from db import get_cursor, get_db
|
|
from routers.profiles import get_pid
|
|
|
|
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
|
|
|
|
|
|
@router.get("/widgets/catalog")
|
|
def get_widgets_catalog(
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
session: dict = Depends(require_auth),
|
|
) -> dict[str, Any]:
|
|
"""Katalog inkl. allowed pro Widget (Feature / Subscription, effektiver Tier)."""
|
|
_ = session
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
return widgets_catalog_payload(pid, conn)
|
|
|
|
|
|
@router.get("/dashboard-layout")
|
|
def get_dashboard_layout(
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
session: dict = Depends(require_auth),
|
|
) -> dict[str, Any]:
|
|
_ = session
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute(
|
|
"SELECT dashboard_layout FROM profiles WHERE id = %s",
|
|
(pid,),
|
|
)
|
|
row = cur.fetchone()
|
|
raw = row["dashboard_layout"] if row else None
|
|
custom, effective = coalesce_effective_layout(raw)
|
|
with get_db() as conn:
|
|
effective = apply_entitlements_to_layout_dict(effective, pid, conn)
|
|
product_adj = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
|
lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn)
|
|
return {
|
|
"custom": custom,
|
|
"layout": effective,
|
|
"product_default_layout": product_adj,
|
|
"lab_default_layout": lab_adj,
|
|
}
|
|
|
|
|
|
@router.put("/dashboard-layout")
|
|
def put_dashboard_layout(
|
|
body: dict[str, Any],
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
session: dict = Depends(require_auth),
|
|
) -> dict[str, Any]:
|
|
_ = session
|
|
pid = get_pid(x_profile_id)
|
|
try:
|
|
payload = DashboardLayoutPayload.model_validate(body)
|
|
except Exception as e:
|
|
raise HTTPException(422, str(e)) from e
|
|
with get_db() as conn:
|
|
adjusted = apply_entitlements_to_layout_dict(payload.to_stored_dict(), pid, conn)
|
|
try:
|
|
payload = DashboardLayoutPayload.model_validate(adjusted)
|
|
except Exception as e:
|
|
raise HTTPException(422, str(e)) from e
|
|
stored = payload.to_stored_dict()
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute(
|
|
"UPDATE profiles SET dashboard_layout = %s WHERE id = %s",
|
|
(Json(stored), pid),
|
|
)
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(404, "Profil nicht gefunden")
|
|
return {"ok": True, "layout": stored}
|
|
|
|
|
|
@router.post("/dashboard-layout/reset")
|
|
def reset_dashboard_layout(
|
|
x_profile_id: Optional[str] = Header(default=None),
|
|
session: dict = Depends(require_auth),
|
|
) -> dict[str, Any]:
|
|
_ = session
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute(
|
|
"UPDATE profiles SET dashboard_layout = NULL WHERE id = %s",
|
|
(pid,),
|
|
)
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(404, "Profil nicht gefunden")
|
|
cleared = apply_entitlements_to_layout_dict(product_default_layout_dict(), pid, conn)
|
|
return {"ok": True, "layout": cleared}
|