feat: Exercises-Router M:N Zuordnungen
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 4s
Test Suite / playwright-tests (push) Failing after 1m57s

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
This commit is contained in:
Lars 2026-04-23 08:51:45 +02:00
parent c7444ecaec
commit d67f659e97
3 changed files with 123 additions and 3 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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 = {