mitai-jinkendo/backend/routers/admin_training_parameters.py
Lars 48508c164e
All checks were successful
Deploy Development / deploy (push) Successful in 59s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s
feat: Add Activity Session Metrics functionality
- Introduced Activity Session Metrics for enhanced tracking of session data.
- Updated backend to support new API endpoints for managing session metrics.
- Added new Pydantic models for activity metrics and replaced metrics functionality.
- Enhanced data layer to include session metrics in recent training session data.
- Updated documentation to reflect changes in session metrics handling.
2026-04-14 11:49:14 +02:00

216 lines
7.1 KiB
Python

"""
Admin: training_parameters catalog (EAV keys for activity session metrics).
Agent guide: .claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md
"""
import re
from typing import Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from psycopg2 import errors as pg_errors
from psycopg2.extras import Json
from auth import require_admin
from db import get_db, get_cursor, r2d
router = APIRouter(prefix="/api/admin/training-parameters", tags=["admin", "training-parameters"])
KEY_PATTERN = re.compile(r"^[a-z][a-z0-9_]{0,62}$")
PARAM_CATEGORY = {"physical", "physiological", "subjective", "environmental", "performance"}
DATA_TYPES = {"integer", "float", "string", "boolean"}
class TrainingParameterCreate(BaseModel):
key: str = Field(..., min_length=1, max_length=50)
name_de: str = Field(..., min_length=1, max_length=100)
name_en: str = Field(..., min_length=1, max_length=100)
category: str = Field(..., max_length=50)
data_type: str = Field(..., max_length=20)
unit: Optional[str] = Field(None, max_length=20)
description_de: Optional[str] = None
description_en: Optional[str] = None
source_field: Optional[str] = Field(None, max_length=100)
validation_rules: Optional[dict] = None
is_active: bool = True
class TrainingParameterUpdate(BaseModel):
name_de: Optional[str] = Field(None, min_length=1, max_length=100)
name_en: Optional[str] = Field(None, min_length=1, max_length=100)
category: Optional[str] = Field(None, max_length=50)
data_type: Optional[str] = Field(None, max_length=20)
unit: Optional[str] = Field(None, max_length=20)
description_de: Optional[str] = None
description_en: Optional[str] = None
source_field: Optional[str] = Field(None, max_length=100)
validation_rules: Optional[dict] = None
is_active: Optional[bool] = None
def _norm_key(key: str) -> str:
k = key.strip().lower()
if not KEY_PATTERN.match(k):
raise HTTPException(
400,
"Ungültiger key: nur Kleinbuchstaben, Ziffern, Unterstriche; muss mit Buchstabe beginnen.",
)
return k
def _validate_category(cat: str) -> str:
c = cat.strip()
if c not in PARAM_CATEGORY:
raise HTTPException(400, f"category muss einer von {sorted(PARAM_CATEGORY)} sein")
return c
def _validate_data_type(dt: str) -> str:
d = dt.strip().lower()
if d not in DATA_TYPES:
raise HTTPException(400, f"data_type muss einer von {sorted(DATA_TYPES)} sein")
return d
@router.get("")
def admin_list_training_parameters(
include_inactive: bool = Query(False),
session: dict = Depends(require_admin),
):
with get_db() as conn:
cur = get_cursor(conn)
if include_inactive:
cur.execute(
"""
SELECT * FROM training_parameters
ORDER BY category, key
"""
)
else:
cur.execute(
"""
SELECT * FROM training_parameters
WHERE is_active = true
ORDER BY category, key
"""
)
return [r2d(r) for r in cur.fetchall()]
@router.post("")
def admin_create_training_parameter(
body: TrainingParameterCreate,
session: dict = Depends(require_admin),
):
key = _norm_key(body.key)
cat = _validate_category(body.category)
dt = _validate_data_type(body.data_type)
rules = body.validation_rules if body.validation_rules is not None else {}
with get_db() as conn:
cur = get_cursor(conn)
try:
cur.execute(
"""
INSERT INTO training_parameters (
key, name_de, name_en, category, data_type, unit,
description_de, description_en, source_field, validation_rules, is_active
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING id
""",
(
key,
body.name_de.strip(),
body.name_en.strip(),
cat,
dt,
body.unit.strip() if body.unit else None,
body.description_de,
body.description_en,
body.source_field.strip() if body.source_field else None,
Json(rules),
body.is_active,
),
)
new_id = cur.fetchone()["id"]
conn.commit()
except pg_errors.UniqueViolation:
conn.rollback()
raise HTTPException(409, "Parameter-key existiert bereits") from None
return {"id": new_id, "key": key}
@router.put("/{param_id}")
def admin_update_training_parameter(
param_id: int,
body: TrainingParameterUpdate,
session: dict = Depends(require_admin),
):
cols: list[str] = []
vals: list[Any] = []
if body.name_de is not None:
cols.append("name_de = %s")
vals.append(body.name_de.strip())
if body.name_en is not None:
cols.append("name_en = %s")
vals.append(body.name_en.strip())
if body.category is not None:
cols.append("category = %s")
vals.append(_validate_category(body.category))
if body.data_type is not None:
cols.append("data_type = %s")
vals.append(_validate_data_type(body.data_type))
if body.unit is not None:
cols.append("unit = %s")
vals.append(body.unit.strip() or None)
if body.description_de is not None:
cols.append("description_de = %s")
vals.append(body.description_de)
if body.description_en is not None:
cols.append("description_en = %s")
vals.append(body.description_en)
if body.source_field is not None:
cols.append("source_field = %s")
vals.append(body.source_field.strip() or None)
if body.validation_rules is not None:
cols.append("validation_rules = %s")
vals.append(Json(body.validation_rules))
if body.is_active is not None:
cols.append("is_active = %s")
vals.append(body.is_active)
if not cols:
raise HTTPException(400, "Keine Felder zum Aktualisieren")
vals.append(param_id)
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
f"UPDATE training_parameters SET {', '.join(cols)} WHERE id = %s RETURNING id",
vals,
)
if not cur.fetchone():
raise HTTPException(404, "Parameter nicht gefunden")
conn.commit()
return {"ok": True, "id": param_id}
@router.delete("/{param_id}")
def admin_deactivate_training_parameter(
param_id: int,
session: dict = Depends(require_admin),
):
"""Soft-delete: is_active = false (FK von session_metrics verhindert hartes Löschen)."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"UPDATE training_parameters SET is_active = false WHERE id = %s RETURNING id",
(param_id,),
)
if not cur.fetchone():
raise HTTPException(404, "Parameter nicht gefunden")
conn.commit()
return {"ok": True, "id": param_id}