From d67f659e9751382f66ab2b1f6ac3cac0446c3c3f Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 23 Apr 2026 08:51:45 +0200 Subject: [PATCH] feat: Exercises-Router M:N Zuordnungen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend API erweitert um M:N Katalog-Zuordnungen: - GET /exercises/{id}: Liefert focus_areas[], training_styles[], target_groups[], age_groups_catalog[] - POST /exercises: Akzeptiert focus_areas_multi[], training_styles_multi[], target_groups_multi[], age_groups_catalog[] - PUT /exercises/{id}: DELETE+INSERT Pattern für M:N Updates (konsistent mit skills) Rückwärtskompatibilität: - Legacy FK-Felder (focus_area_id, training_style_id, training_character_id) bleiben erhalten - Alte Aufrufe funktionieren unverändert - Neue M:N Felder sind optional version: 0.3.1 modules: exercises 0.4.0 --- backend/routers/exercises.py | 109 +++++++++++++++++++++++++++++++++++ backend/version.py | 15 ++++- frontend/src/version.js | 2 +- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 706c4d9..5520e6a 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -143,6 +143,45 @@ def get_exercise(exercise_id: int, session=Depends(require_auth)): """, (exercise_id,)) exercise['media'] = [r2d(r) for r in cur.fetchall()] + # Get M:N catalog assignments + # Focus Areas + cur.execute(""" + SELECT efa.*, fa.name, fa.abbreviation, fa.color + FROM exercise_focus_areas efa + JOIN focus_areas fa ON efa.focus_area_id = fa.id + WHERE efa.exercise_id = %s + ORDER BY efa.is_primary DESC, fa.name + """, (exercise_id,)) + exercise['focus_areas'] = [r2d(r) for r in cur.fetchall()] + + # Training Styles + cur.execute(""" + SELECT es.*, ts.name, ts.abbreviation + FROM exercise_styles es + JOIN training_styles ts ON es.training_style_id = ts.id + WHERE es.exercise_id = %s + ORDER BY es.is_primary DESC, ts.name + """, (exercise_id,)) + exercise['training_styles'] = [r2d(r) for r in cur.fetchall()] + + # Target Groups + cur.execute(""" + SELECT etg.*, tg.name, tg.description + FROM exercise_target_groups etg + JOIN target_groups tg ON etg.target_group_id = tg.id + WHERE etg.exercise_id = %s + ORDER BY etg.is_primary DESC, tg.name + """, (exercise_id,)) + exercise['target_groups'] = [r2d(r) for r in cur.fetchall()] + + # Age Groups + cur.execute(""" + SELECT age_group FROM exercise_age_groups + WHERE exercise_id = %s + ORDER BY age_group + """, (exercise_id,)) + exercise['age_groups_catalog'] = [r['age_group'] for r in cur.fetchall()] + return exercise @@ -227,6 +266,39 @@ def create_exercise(data: dict, session=Depends(require_auth)): skill.get('target_level') )) + # Add M:N catalog assignments if provided + # Focus Areas + if data.get('focus_areas_multi'): + for fa in data['focus_areas_multi']: + cur.execute(""" + INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, fa['focus_area_id'], fa.get('is_primary', False))) + + # Training Styles + if data.get('training_styles_multi'): + for ts in data['training_styles_multi']: + cur.execute(""" + INSERT INTO exercise_styles (exercise_id, training_style_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, ts['training_style_id'], ts.get('is_primary', False))) + + # Target Groups + if data.get('target_groups_multi'): + for tg in data['target_groups_multi']: + cur.execute(""" + INSERT INTO exercise_target_groups (exercise_id, target_group_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, tg['target_group_id'], tg.get('is_primary', False))) + + # Age Groups + if data.get('age_groups_catalog'): + for age_group in data['age_groups_catalog']: + cur.execute(""" + INSERT INTO exercise_age_groups (exercise_id, age_group) + VALUES (%s, %s) + """, (exercise_id, age_group)) + conn.commit() return get_exercise(exercise_id, session) @@ -313,6 +385,43 @@ def update_exercise(exercise_id: int, data: dict, session=Depends(require_auth)) skill.get('target_level') )) + # Update M:N catalog assignments if provided + # Focus Areas + if 'focus_areas_multi' in data: + cur.execute("DELETE FROM exercise_focus_areas WHERE exercise_id = %s", (exercise_id,)) + for fa in data['focus_areas_multi']: + cur.execute(""" + INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, fa['focus_area_id'], fa.get('is_primary', False))) + + # Training Styles + if 'training_styles_multi' in data: + cur.execute("DELETE FROM exercise_styles WHERE exercise_id = %s", (exercise_id,)) + for ts in data['training_styles_multi']: + cur.execute(""" + INSERT INTO exercise_styles (exercise_id, training_style_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, ts['training_style_id'], ts.get('is_primary', False))) + + # Target Groups + if 'target_groups_multi' in data: + cur.execute("DELETE FROM exercise_target_groups WHERE exercise_id = %s", (exercise_id,)) + for tg in data['target_groups_multi']: + cur.execute(""" + INSERT INTO exercise_target_groups (exercise_id, target_group_id, is_primary) + VALUES (%s, %s, %s) + """, (exercise_id, tg['target_group_id'], tg.get('is_primary', False))) + + # Age Groups + if 'age_groups_catalog' in data: + cur.execute("DELETE FROM exercise_age_groups WHERE exercise_id = %s", (exercise_id,)) + for age_group in data['age_groups_catalog']: + cur.execute(""" + INSERT INTO exercise_age_groups (exercise_id, age_group) + VALUES (%s, %s) + """, (exercise_id, age_group)) + conn.commit() return get_exercise(exercise_id, session) diff --git a/backend/version.py b/backend/version.py index 961bfda..8198e79 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.3.0" +APP_VERSION = "0.3.1" BUILD_DATE = "2026-04-23" DB_SCHEMA_VERSION = "20260423" @@ -11,7 +11,7 @@ MODULE_VERSIONS = { "groups": "0.1.0", "skills": "0.1.0", "methods": "0.1.0", - "exercises": "0.3.0", # Updated: M:N Beziehungen + "exercises": "0.4.0", # Updated: M:N API-Integration "training_units": "0.1.0", "training_programs": "0.1.0", "planning": "0.1.0", @@ -22,6 +22,17 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.3.1", + "date": "2026-04-23", + "changes": [ + "Feature: Exercises-Router unterstützt M:N Zuordnungen", + "API: GET /exercises/{id} liefert focus_areas[], training_styles[], target_groups[], age_groups_catalog[]", + "API: POST/PUT /exercises akzeptiert focus_areas_multi[], training_styles_multi[], target_groups_multi[], age_groups_catalog[]", + "Pattern: DELETE+INSERT für M:N Updates (konsistent mit skills)", + "Backward-Compatible: Legacy FK-Felder (focus_area_id, training_style_id) bleiben erhalten", + ] + }, { "version": "0.3.0", "date": "2026-04-23", diff --git a/frontend/src/version.js b/frontend/src/version.js index 6a81386..3c93a65 100644 --- a/frontend/src/version.js +++ b/frontend/src/version.js @@ -1,6 +1,6 @@ // Shinkan Jinkendo Frontend Version -export const APP_VERSION = "0.3.0" +export const APP_VERSION = "0.3.1" export const BUILD_DATE = "2026-04-23" export const PAGE_VERSIONS = {