diff --git a/backend/routers/maturity_models.py b/backend/routers/maturity_models.py index ee5719e..1bd528f 100644 --- a/backend/routers/maturity_models.py +++ b/backend/routers/maturity_models.py @@ -260,7 +260,18 @@ def _binding_matches_query_dims( b_style: Any, b_tt: Any, ) -> bool: - """Zeile passt zur Abfrage, wenn gesetzte Dimensionswerte der Zeile mit der Anfrage übereinstimmen.""" + """ + Prüft, ob eine Binding-Zeile zur Anfrage passt. + + Nicht gesetzte Spalten der Zeile gelten als „egal“ (Wildcard): + - Nur Fokus: Stilrichtung und Trainingsstil NULL → gilt für alle Stile/Trainingsstile unter diesem Fokus. + - Fokus + Stilrichtung: Trainingsstil NULL → gilt nur für diese Stilrichtung, für jeden Trainingsstil. + - Fokus + Trainingsstil: Stilrichtung NULL → gilt nur für diesen Trainingsstil, für jede Stilrichtung. + - Alle drei gesetzt → ausschließlich diese Kombination. + + In der Anfrage fehlende Dimensionen schließen Zeilen aus, die diese Dimension festgelegt haben + (z. B. Fokus+Trainingsstil-Binding zählt nicht, wenn kein Trainingsstil angefragt wird). + """ if b_style is not None: if style_direction_id is None or int(b_style) != int(style_direction_id): return False @@ -279,6 +290,18 @@ def _binding_dim_count(b_style: Any, b_tt: Any) -> int: return n +def _focus_has_any_bindings(cur, focus_area_id: int) -> bool: + cur.execute( + """ + SELECT 1 FROM maturity_model_context_bindings + WHERE focus_area_id = %s + LIMIT 1 + """, + (focus_area_id,), + ) + return cur.fetchone() is not None + + def _resolve_binding_model_ids( cur, focus_area_id: int, @@ -449,6 +472,8 @@ def resolve_maturity_model( Zuordnungen überschreiben Zelltexte gleicher Fähigkeit/Stufe. **Priorität 2 (Legacy):** Ein aktives Modell per M:N am Modell (Zielgruppe unverändert). + Nur wenn für den Fokusbereich **keine** Einträge in `maturity_model_context_bindings` existieren — + sonst würde Legacy den Trainingsstil ignorieren und Kontext-Bindings unterlaufen. """ with get_db() as conn: cur = get_cursor(conn) @@ -462,6 +487,8 @@ def resolve_maturity_model( if chain: loaded = [_load_full_model(cur, mid) for mid in chain] return _merge_loaded_models(loaded) + if _focus_has_any_bindings(cur, int(focus_area_id)): + return None mid = _legacy_resolve_pick_model_id( cur, focus_area_id, style_direction_id, target_group_id @@ -1188,13 +1215,19 @@ def export_resolved_maturity_bundle( loaded = [_load_full_model(cur, mid) for mid in chain] merged = _merge_loaded_models(loaded) else: + if _focus_has_any_bindings(cur, int(focus_area_id)): + raise HTTPException( + 404, + "Kein Reifegradmodell für diese Kontext-Kombination (es gibt Bindings für diesen Fokus, " + "aber keine passende Zeile).", + ) mid = _legacy_resolve_pick_model_id( cur, focus_area_id, style_direction_id, None ) if mid is None: raise HTTPException( 404, - "Kein Modell für diesen Kontext (keine Bindings und kein Legacy-Treffer)", + "Kein Modell für diesen Kontext (keine Bindings für den Fokus und kein Legacy-Treffer).", ) merged = _load_full_model(cur, mid) diff --git a/backend/version.py b/backend/version.py index f24274d..5cac34f 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.7.4" +APP_VERSION = "0.7.5" BUILD_DATE = "2026-04-27" DB_SCHEMA_VERSION = "20260427027" @@ -19,10 +19,17 @@ MODULE_VERSIONS = { "admin": "1.0.0", "membership": "1.0.0", "catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012) - "maturity_models": "1.3.0", # 027: Fokus+Trainingsstil; Export/Import; resolve-Merge + "maturity_models": "1.3.1", # Resolve: kein Legacy, wenn Fokus bereits Bindings hat } CHANGELOG = [ + { + "version": "0.7.5", + "date": "2026-04-27", + "changes": [ + "Resolve/Export: Legacy M:N nur noch, wenn für den Fokus keine Kontext-Bindings existieren (korrekte Striktheit für Fokus+Trainingsstil / Teil-Kontexte)", + ], + }, { "version": "0.7.4", "date": "2026-04-27", diff --git a/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx b/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx index f3bad93..79638ee 100644 --- a/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx +++ b/frontend/src/components/admin/MaturityModelBindingsAdmin.jsx @@ -132,11 +132,16 @@ export default function MaturityModelBindingsAdmin() {
Zusammenführung: Zu einem Fokus können mehrere Zeilen existieren (nur Fokus, Fokus +
- Stilrichtung, Fokus + Trainingsstil ohne Stil, oder alle drei). Beim Aufruf von{' '}
- /maturity-models/resolve werden alle zur Anfrage passenden
- Zeilen ermittelt, nach Spezifität sortiert (weniger zuerst) und zu einer Matrix verbunden: spezifischere
- Zuordnungen überschreiben Zelltexte gleicher Fähigkeit und Stufe. Stufen (Spalten) stammen vom{' '}
- ersten (am wenigsten spezifischen) Modell in dieser Kette.
+ Stilrichtung, Fokus + Trainingsstil ohne Stil, oder alle drei). Eine Zeile gilt nur, wenn alle von ihr
+ gesetzten Dimensionen mit der Anfrage übereinstimmen; fehlende Spalten in der Zeile bedeuten „alle“
+ (z. B. Fokus+Trainingsstil gilt unter jeder Stilrichtung, aber nur für genau diesen Trainingsstil).
+ Sobald für einen Fokusbereich mindestens eine Zuordnung in dieser Tabelle existiert, wird{' '}
+ kein älteres Legacy-Resolve (nur M:N am Modell) mehr verwendet — fehlende Treffer
+ liefern dann keine Matrix. Beim Aufruf von{' '}
+ /maturity-models/resolve werden alle passenden Zeilen nach
+ Spezifität sortiert (weniger zuerst) gemerged; spezifischere Zuordnungen überschreiben Zelltexte gleicher
+ Fähigkeit und Stufe. Stufen (Spalten) stammen vom ersten (am wenigsten spezifischen)
+ Modell in dieser Kette.