mitai-jinkendo/backend/dashboard_layout_schema.py
Lars 141df021c1
All checks were successful
Deploy Development / deploy (push) Successful in 1m2s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
refactor: rename Dashboard-Lab-Widgets to Dashboard-Widgets and update related documentation
- Renamed references from "Dashboard-Lab-Widgets" to "Dashboard-Widgets" across documentation and codebase for consistency.
- Removed the deprecated Dashboard-Lab page and integrated its functionality into the new Dashboard-Widgets layout.
- Updated widget registration and configuration handling to reflect the new naming convention.
- Adjusted documentation in `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` and other related files to ensure clarity on the updated structure.
- Bumped application version to reflect these changes.
2026-04-23 16:18:10 +02:00

149 lines
5.0 KiB
Python

"""
Dashboard-Layout v1: Validierung, Produkt-Standard (Übersicht) und Servertemplate (`lab_default_layout_dict`).
Erlaubte Widget-IDs und Reihenfolge: widget_catalog.WIDGET_CATALOG.
"""
from __future__ import annotations
import copy
from typing import Any, Literal
from pydantic import BaseModel, Field, field_validator, model_validator
from dashboard_widget_config import validate_widget_entry_config
from widget_catalog import (
ALLOWED_WIDGET_IDS,
DEFAULT_LAB_WIDGET_IDS,
DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS,
WIDGET_CATALOG,
)
# Abwärtskompatibel (Tests importieren weiterhin aus diesem Modul)
__all__ = [
"ALLOWED_WIDGET_IDS",
"DashboardLayoutPayload",
"DashboardWidgetEntry",
"coalesce_effective_layout",
"default_layout_dict",
"lab_default_layout_dict",
"merge_missing_catalog_widgets",
"product_default_layout_dict",
]
def lab_default_layout_dict() -> dict[str, Any]:
"""Serverseitiges Standardlayout (DEFAULT_LAB_WIDGET_IDS); API-Feld `lab_default_layout`, u. a. für Editor/Reset."""
on = DEFAULT_LAB_WIDGET_IDS
return {
"version": 1,
"widgets": [{"id": e["id"], "enabled": e["id"] in on} for e in WIDGET_CATALOG],
}
def product_default_layout_dict() -> dict[str, Any]:
"""Code-Fallback für die Produkt-Übersicht; live-Standard ggf. system_config (siehe get_product_default_base_dict)."""
on = DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS
return {
"version": 1,
"widgets": [{"id": e["id"], "enabled": e["id"] in on} for e in WIDGET_CATALOG],
}
def default_layout_dict() -> dict[str, Any]:
"""Alias: Produkt-Standard (coalesce, Reset). Lab nutzt lab_default_layout_dict()."""
return product_default_layout_dict()
def merge_missing_catalog_widgets(layout: dict[str, Any]) -> dict[str, Any]:
"""
Hängt fehlende Widget-IDs aus WIDGET_CATALOG an (enabled=False, leere config).
Bestehende Reihenfolge bleibt erhalten — nötig, damit neue Katalog-Einträge in
„Übersicht anpassen“ / Lab erscheinen, ohne dass Nutzer:innen das Layout resetten müssen.
"""
out = copy.deepcopy(layout)
widgets: list[dict[str, Any]] = list(out.get("widgets") or [])
seen: set[str] = {str(w["id"]) for w in widgets if w.get("id")}
for e in WIDGET_CATALOG:
wid = e["id"]
if wid not in seen:
widgets.append({"id": wid, "enabled": False, "config": {}})
seen.add(wid)
out["version"] = out.get("version", 1)
out["widgets"] = widgets
return out
class DashboardWidgetEntry(BaseModel):
id: str = Field(min_length=1, max_length=64)
enabled: bool = True
config: dict[str, Any] = Field(default_factory=dict)
@field_validator("config", mode="before")
@classmethod
def _config_coerce(cls, v: Any) -> dict[str, Any]:
if v is None:
return {}
if not isinstance(v, dict):
raise ValueError("config muss Objekt sein")
return v
@model_validator(mode="after")
def _normalize_widget_config(self) -> DashboardWidgetEntry:
normalized = validate_widget_entry_config(self.id, self.config)
return self.model_copy(update={"config": normalized})
class DashboardLayoutPayload(BaseModel):
version: Literal[1] = 1
widgets: list[DashboardWidgetEntry] = Field(min_length=1, max_length=32)
@model_validator(mode="after")
def _validate_widgets(self) -> DashboardLayoutPayload:
ids = [w.id for w in self.widgets]
if len(ids) != len(set(ids)):
raise ValueError("Doppelte widget id")
bad = [i for i in ids if i not in ALLOWED_WIDGET_IDS]
if bad:
raise ValueError(f"Unbekannte Widget-IDs: {bad}")
if not any(w.enabled for w in self.widgets):
raise ValueError("Mindestens ein Widget muss aktiv sein")
return self
def to_stored_dict(self) -> dict[str, Any]:
out_widgets: list[dict[str, Any]] = []
for w in self.widgets:
d: dict[str, Any] = {"id": w.id, "enabled": w.enabled}
if w.config:
d["config"] = dict(w.config)
out_widgets.append(d)
return {"version": self.version, "widgets": out_widgets}
def coalesce_effective_layout(raw: Any) -> tuple[bool, dict[str, Any]]:
"""
Returns (has_custom, effective_layout).
has_custom=True nur wenn DB-Wert vorhanden und gültig (v1).
"""
if raw is None:
return False, default_layout_dict()
parsed_obj: Any = raw
if isinstance(raw, str):
import json
try:
parsed_obj = json.loads(raw)
except json.JSONDecodeError:
return False, default_layout_dict()
if not isinstance(parsed_obj, dict):
return False, default_layout_dict()
try:
parsed = DashboardLayoutPayload.model_validate(
{
"version": parsed_obj.get("version", 1),
"widgets": parsed_obj.get("widgets", []),
}
)
return True, parsed.to_stored_dict()
except Exception:
return False, default_layout_dict()