feat: Add CSV import support for additional training parameters
- 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:
parent
7d6fdab812
commit
026c51b6b5
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user