mitai-jinkendo/backend/reference_value_validation.py
Lars 45e4e64f15
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
feat: Enhance reference value types management with validation rules and metadata
- 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.
2026-04-06 21:25:42 +02:00

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