- Added validation for widget configuration in the DashboardWidgetEntry model to ensure proper data structure. - Updated the DashboardLayoutPayload to include widget configuration in the serialized output. - Improved the PilotBodySection and DashboardLabPage components to support dynamic chart days configuration for the body overview widget. - Refactored layout editor functions to normalize widget configurations for better handling. - Bumped app_dashboard version to 1.2.0 to reflect the new features and improvements.
105 lines
3.3 KiB
Python
105 lines
3.3 KiB
Python
"""
|
|
Dashboard-Layout v1 (Nutzer-Lab): Validierung und Standard-Layout.
|
|
|
|
Erlaubte Widget-IDs und Standard-Reihenfolge: widget_catalog.WIDGET_CATALOG
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
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, WIDGET_CATALOG
|
|
|
|
# Abwärtskompatibel (Tests importieren weiterhin aus diesem Modul)
|
|
__all__ = [
|
|
"ALLOWED_WIDGET_IDS",
|
|
"DashboardLayoutPayload",
|
|
"DashboardWidgetEntry",
|
|
"coalesce_effective_layout",
|
|
"default_layout_dict",
|
|
]
|
|
|
|
|
|
def default_layout_dict() -> dict[str, Any]:
|
|
return {
|
|
"version": 1,
|
|
"widgets": [{"id": e["id"], "enabled": True} for e in WIDGET_CATALOG],
|
|
}
|
|
|
|
|
|
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()
|