- Updated the backend to include new fields for validation rules and metadata in reference value types. - Enhanced the AdminReferenceValueTypesPage to support new validation rules for different data types. - Improved the ProfileReferenceValuesPage to handle validation and metadata for profile reference values. - Added API endpoint for fetching reference value metadata enums to support frontend validation. - Refactored frontend forms to incorporate new fields and validation logic for a better user experience.
168 lines
5.4 KiB
Python
168 lines
5.4 KiB
Python
"""
|
|
Validierung & Konstanten für persönliche Referenzwerte.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from fastapi import HTTPException
|
|
|
|
REF_VALUE_SOURCES = frozenset(
|
|
{
|
|
"manual_user",
|
|
"manual_admin",
|
|
"import_device",
|
|
"import_app",
|
|
"derived_system",
|
|
"estimated_system",
|
|
"test_entry",
|
|
}
|
|
)
|
|
|
|
REF_VALUE_METHODS = frozenset(
|
|
{
|
|
"direct_measurement",
|
|
"lab_test",
|
|
"field_test",
|
|
"questionnaire",
|
|
"formula_estimation",
|
|
"trend_analysis",
|
|
"device_algorithm",
|
|
"manual_assessment",
|
|
"imported_external",
|
|
"unknown",
|
|
}
|
|
)
|
|
|
|
|
|
REF_VALUE_CONFIDENCE = frozenset({"high", "medium", "low", "unknown"})
|
|
|
|
VALUE_DATA_TYPES = frozenset({"integer", "decimal", "percentage", "text", "enum"})
|
|
|
|
|
|
def _rules_dict(raw: Any) -> dict:
|
|
if not raw:
|
|
return {}
|
|
if isinstance(raw, dict):
|
|
return raw
|
|
return {}
|
|
|
|
|
|
def validate_meta_source(source: Optional[str]) -> str:
|
|
if not source or not str(source).strip():
|
|
raise HTTPException(400, "Quelle (source) ist erforderlich.")
|
|
s = str(source).strip()
|
|
if s not in REF_VALUE_SOURCES:
|
|
raise HTTPException(400, f"Ungültige Quelle: {s}")
|
|
return s
|
|
|
|
|
|
def validate_meta_method(method: Optional[str]) -> str:
|
|
if not method or not str(method).strip():
|
|
raise HTTPException(400, "Methode (method) ist erforderlich.")
|
|
m = str(method).strip()
|
|
if m not in REF_VALUE_METHODS:
|
|
raise HTTPException(400, f"Ungültige Methode: {m}")
|
|
return m
|
|
|
|
|
|
def validate_meta_confidence(confidence: Optional[str]) -> str:
|
|
if not confidence or not str(confidence).strip():
|
|
raise HTTPException(400, "Vertrauensgrad (confidence) ist erforderlich.")
|
|
c = str(confidence).strip().lower()
|
|
if c not in REF_VALUE_CONFIDENCE:
|
|
raise HTTPException(400, f"Ungültiger Vertrauensgrad: {c}")
|
|
return c
|
|
|
|
|
|
def resolve_unit_from_type(default_unit: Optional[str]) -> str:
|
|
u = (default_unit or "").strip()
|
|
if not u:
|
|
raise HTTPException(
|
|
400,
|
|
"Für diesen Kennwert-Typ ist keine Einheit hinterlegt. Bitte im Admin einen Standard unter „Standard-Einheit“ setzen.",
|
|
)
|
|
return u
|
|
|
|
|
|
def validate_value_for_data_type(
|
|
value_data_type: str,
|
|
validation_rules_raw: Any,
|
|
value_numeric: Optional[float],
|
|
value_text: Optional[str],
|
|
) -> tuple[Optional[float], Optional[str]]:
|
|
"""
|
|
Je nach value_data_type den Wert prüfen und (value_numeric, value_text) für die DB liefern.
|
|
"""
|
|
vdt = (value_data_type or "decimal").strip().lower()
|
|
if vdt not in VALUE_DATA_TYPES:
|
|
raise HTTPException(400, f"Ungültiger interner Datentyp: {vdt}")
|
|
|
|
rules = _rules_dict(validation_rules_raw)
|
|
|
|
if vdt in ("integer", "decimal", "percentage"):
|
|
if value_numeric is None:
|
|
raise HTTPException(400, "Bitte einen numerischen Wert eingeben.")
|
|
v = float(value_numeric)
|
|
if vdt == "integer":
|
|
if abs(v - round(v)) > 1e-9:
|
|
raise HTTPException(400, "Der Wert muss eine ganze Zahl sein.")
|
|
v = float(int(round(v)))
|
|
pos = bool(rules.get("positive_only"))
|
|
mn = rules.get("min")
|
|
mx = rules.get("max")
|
|
if mn is not None:
|
|
mn = float(mn)
|
|
if mx is not None:
|
|
mx = float(mx)
|
|
if vdt == "percentage":
|
|
gmn = float(mn) if mn is not None else 0.0
|
|
gmx = float(mx) if mx is not None else 100.0
|
|
gmn = max(gmn, 0.0)
|
|
gmx = min(gmx, 100.0)
|
|
if gmn > gmx:
|
|
raise HTTPException(500, "Ungültige Plausibilisierung: min > max (Prozent).")
|
|
if v < gmn or v > gmx:
|
|
raise HTTPException(
|
|
400,
|
|
f"Prozentwert muss zwischen {gmn} und {gmx} liegen.",
|
|
)
|
|
if pos and v <= 0:
|
|
raise HTTPException(400, "Prozentwert muss positiv sein (laut Konfiguration).")
|
|
else:
|
|
if pos and v <= 0:
|
|
raise HTTPException(400, "Der Wert muss positiv sein (laut Konfiguration).")
|
|
if mn is not None and v < mn:
|
|
raise HTTPException(400, f"Der Wert muss mindestens {mn} sein.")
|
|
if mx is not None and v > mx:
|
|
raise HTTPException(400, f"Der Wert darf höchstens {mx} sein.")
|
|
return v, None
|
|
|
|
# text / enum
|
|
s = (value_text or "").strip() if value_text is not None else ""
|
|
if vdt == "text" and not s and not rules.get("not_empty"):
|
|
return None, ""
|
|
if rules.get("not_empty") and not s:
|
|
raise HTTPException(400, "Der Text darf nicht leer sein.")
|
|
ml = rules.get("max_length")
|
|
if ml is not None:
|
|
try:
|
|
ml_int = int(ml)
|
|
except (TypeError, ValueError):
|
|
ml_int = None
|
|
if ml_int is not None and len(s) > ml_int:
|
|
raise HTTPException(400, f"Text zu lang (max. {ml_int} Zeichen).")
|
|
if vdt == "enum":
|
|
allowed = rules.get("allowed_values") or []
|
|
if not isinstance(allowed, list):
|
|
allowed = []
|
|
allowed_str = [str(x).strip() for x in allowed if str(x).strip()]
|
|
if not allowed_str:
|
|
raise HTTPException(500, "ENUM-Typ ohne erlaubte Werte (Admin-Konfiguration).")
|
|
if s not in allowed_str:
|
|
raise HTTPException(
|
|
400,
|
|
f"Ungültiger Wert. Erlaubt: {', '.join(allowed_str)}",
|
|
)
|
|
return None, s
|