feat: Add CSV import support for additional training parameters
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- Introduced `resolve_activity_attribute_schema_for_csv_import` to enhance the handling of training parameters during CSV imports, allowing for the inclusion of active parameters not present in the category/type profile.
- Updated `apply_activity_mapped_column_aliases` and `upsert_session_metrics_from_csv_mapped` to utilize the new CSV import function, ensuring comprehensive mapping and insertion of metrics.
- Added unit tests to validate the new functionality and ensure correct behavior when handling mapped training parameters during CSV imports.
This commit is contained in:
Lars 2026-04-16 11:32:16 +02:00
parent 7d6fdab812
commit 026c51b6b5
2 changed files with 152 additions and 2 deletions

View File

@ -174,6 +174,74 @@ def resolve_activity_attribute_schema(
return merge_parameter_schema_rows(category_rows, type_rows)
def resolve_activity_attribute_schema_for_csv_import(
cur,
training_category: Optional[str],
training_type_id: Optional[int],
mapped: Mapping[str, Any],
) -> List[Dict[str, Any]]:
"""
Wie resolve_activity_attribute_schema, plus alle aktiven training_parameters, deren key in
``mapped`` vorkommt (nicht Modul-Registry), aber nicht in Kategorie/Typ-Profil z. B. wenn
``activity_type_mappings`` fehlt oder der Parameter nur für andere Typen gebucht ist.
Damit schreibt der Universal-CSV-Import EAV für gültig gemappte Zielfelder wie bei cd29c7d,
sobald der Parameter in ``training_parameters`` existiert.
"""
base = resolve_activity_attribute_schema(cur, training_category, training_type_id)
by_key: Dict[str, Dict[str, Any]] = {s["key"]: s for s in base}
for k, raw in mapped.items():
if raw is None or raw == "":
continue
if k in by_key:
continue
if k in ACTIVITY_MODULE_REGISTRY_FIELD_KEYS:
continue
cur.execute(
"""
SELECT
id,
key,
name_de,
name_en,
category AS param_category,
data_type,
unit,
validation_rules,
source_field
FROM training_parameters
WHERE key = %s AND is_active = true
LIMIT 1
""",
(k,),
)
row = cur.fetchone()
if not row:
continue
pid = int(row["id"])
if any(s["training_parameter_id"] == pid for s in by_key.values()):
continue
by_key[k] = {
"training_parameter_id": pid,
"key": row["key"],
"name_de": row["name_de"],
"name_en": row["name_en"],
"param_category": row["param_category"],
"data_type": row["data_type"],
"unit": row["unit"],
"validation_rules": row["validation_rules"] or {},
"source_field": row["source_field"],
"sort_order": 100_000,
"required": False,
"ui_group": None,
}
out = list(by_key.values())
out.sort(key=lambda x: (x.get("sort_order", 0), x["key"]))
return out
def _validation_rules_dict(raw: Any) -> Dict[str, Any]:
if isinstance(raw, dict):
return raw
@ -286,7 +354,9 @@ def apply_activity_mapped_column_aliases(
training_category: Optional[str],
training_type_id: Optional[int],
) -> Dict[str, Any]:
schema = resolve_activity_attribute_schema(cur, training_category, training_type_id)
schema = resolve_activity_attribute_schema_for_csv_import(
cur, training_category, training_type_id, mapped
)
return apply_activity_mapped_column_aliases_from_schema(mapped, schema)
@ -311,7 +381,9 @@ def upsert_session_metrics_from_csv_mapped(
row = cur.fetchone()
if not row or str(row["profile_id"]) != str(profile_id):
return
schema = resolve_activity_attribute_schema(cur, training_category, training_type_id)
schema = resolve_activity_attribute_schema_for_csv_import(
cur, training_category, training_type_id, mapped
)
for spec in schema:
pkey = spec["key"]
if pkey not in mapped:

View File

@ -13,6 +13,7 @@ from data_layer.activity_session_metrics import (
merge_parameter_schema_rows,
replace_activity_session_metrics,
resolve_activity_attribute_schema,
resolve_activity_attribute_schema_for_csv_import,
upsert_session_metrics_from_csv_mapped,
_row_value_tuple,
_validate_single_value,
@ -323,6 +324,83 @@ def test_apply_mapped_aliases_does_not_overwrite_existing_column():
assert out["hr_avg"] == 120
@patch("data_layer.activity_session_metrics.resolve_activity_attribute_schema", return_value=[])
def test_resolve_csv_import_adds_mapped_keys_from_training_parameters(mock_resolve):
"""Ohne Kategorie/Typ-Profil: gemappte Keys aus training_parameters ergänzen."""
class Cur:
def __init__(self):
self.sql = ""
def execute(self, sql, params=None):
self.sql = sql
self.params = params
def fetchone(self):
if "training_parameters" in self.sql:
return {
"id": 7,
"key": "cadence",
"name_de": "Kadenz",
"name_en": "Cadence",
"param_category": "physical",
"data_type": "integer",
"unit": "rpm",
"validation_rules": {},
"source_field": None,
}
return None
cur = Cur()
out = resolve_activity_attribute_schema_for_csv_import(cur, None, None, {"cadence": 90})
assert len(out) == 1
assert out[0]["key"] == "cadence"
assert out[0]["training_parameter_id"] == 7
mock_resolve.assert_called_once_with(cur, None, None)
@patch("data_layer.activity_session_metrics.resolve_activity_attribute_schema", return_value=[])
def test_upsert_csv_inserts_eav_when_only_mapped_training_parameter(mock_resolve):
class Cur:
def __init__(self):
self.asm_inserts = 0
self.sql = ""
def execute(self, sql, params=None):
self.sql = sql
self.params = params
if "INSERT INTO activity_session_metrics" in sql:
self.asm_inserts += 1
def fetchone(self):
if "activity_log" in self.sql:
return {"profile_id": "00000000-0000-0000-0000-000000000001"}
if "training_parameters" in self.sql:
return {
"id": 7,
"key": "cadence",
"name_de": "K",
"name_en": "K",
"param_category": "physical",
"data_type": "integer",
"unit": None,
"validation_rules": {"min": 0, "max": 300},
"source_field": None,
}
return None
cur = Cur()
upsert_session_metrics_from_csv_mapped(
cur,
"00000000-0000-0000-0000-000000000001",
"00000000-0000-0000-0000-000000000002",
{"cadence": 90},
None,
None,
)
assert cur.asm_inserts == 1
@patch("data_layer.activity_session_metrics.resolve_activity_attribute_schema")
def test_upsert_csv_skips_parameter_with_source_field(mock_schema):
"""Kein INSERT in activity_session_metrics für Spalten-Parameter (avg_hr → hr_avg)."""