shinkan-jinkendo/backend/routers/exercises.py

892 lines
32 KiB
Python
Raw Normal View History

"""
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
Exercises Router - v2.0 (Clean-Room Rebuild)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
Komplett neu gebaut nach EXERCISES_API_SPEC.md v1.2
KEIN Legacy-Code aus v1 - nur M:N Relations, keine JSONB-Felder für Kataloge
"""
import hashlib
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
import json
import logging
import os
from pathlib import Path
from typing import Optional
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File, Form
from pydantic import BaseModel, Field, model_validator
from db import get_db, get_cursor, r2d
from auth import require_auth
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["exercises"])
MEDIA_ROOT = Path(os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent.parent / "media")))
MAX_EXERCISE_MEDIA = 10
MAX_UPLOAD_BYTES = 50 * 1024 * 1024
ALLOWED_UPLOAD_MIMES = frozenset(
{"image/jpeg", "image/png", "image/gif", "video/mp4", "application/pdf"}
)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# ============================================================================
# Pydantic Models
# ============================================================================
class ExerciseCreate(BaseModel):
# Basis-Felder (goal/execution: DB-Constraint mind. eines; Wiki oft nur eines)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
title: str = Field(..., min_length=3, max_length=300)
summary: Optional[str] = None
goal: Optional[str] = Field(None, max_length=5000)
execution: Optional[str] = Field(None, max_length=10000)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
preparation: Optional[str] = None
trainer_notes: Optional[str] = None
# Dauer & Gruppengröße
duration_min: Optional[int] = None
duration_max: Optional[int] = None
group_size_min: Optional[int] = None
group_size_max: Optional[int] = None
# Equipment (Liste von Strings)
equipment: list[str] = []
# M:N Relations (Liste von {id: int, is_primary: bool})
focus_areas_multi: list[dict] = []
training_styles_multi: list[dict] = []
training_types_multi: list[dict] = []
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
target_groups_multi: list[dict] = []
age_groups: list[str] = [] # ["Kinder", "Teenager"] aus Katalog
# Skills (Liste von {skill_id: int, is_primary: bool, intensity: str, required_level: str, target_level: str})
skills: list[dict] = []
# Sichtbarkeit & Status
visibility: str = "private"
status: str = "draft"
club_id: Optional[int] = None
@model_validator(mode="after")
def normalize_goal_execution(self):
g = (self.goal or "").strip() or None
e = (self.execution or "").strip() or None
if not g and not e:
raise ValueError("Mindestens eines der Felder Ziel oder Durchführung ist erforderlich")
self.goal = g
self.execution = e
return self
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
class ExerciseUpdate(BaseModel):
# Alle Felder optional für Partial Update
title: Optional[str] = Field(None, min_length=3, max_length=300)
summary: Optional[str] = None
goal: Optional[str] = Field(None, max_length=5000)
execution: Optional[str] = Field(None, max_length=10000)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
preparation: Optional[str] = None
trainer_notes: Optional[str] = None
duration_min: Optional[int] = None
duration_max: Optional[int] = None
group_size_min: Optional[int] = None
group_size_max: Optional[int] = None
equipment: Optional[list[str]] = None
focus_areas_multi: Optional[list[dict]] = None
training_styles_multi: Optional[list[dict]] = None
training_types_multi: Optional[list[dict]] = None
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
target_groups_multi: Optional[list[dict]] = None
age_groups: Optional[list[str]] = None
skills: Optional[list[dict]] = None
visibility: Optional[str] = None
status: Optional[str] = None
club_id: Optional[int] = None
@model_validator(mode="after")
def normalize_goal_execution(self):
if self.goal is not None:
self.goal = self.goal.strip() or None
if self.execution is not None:
self.execution = self.execution.strip() or None
return self
class ExerciseMediaUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
is_primary: Optional[bool] = None
context: Optional[str] = None
class ExerciseMediaReorder(BaseModel):
media_ids: list[int]
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# ============================================================================
# Helper Functions
# ============================================================================
def _row_created_by(row) -> int:
if row is None:
return None
if isinstance(row, dict):
return row.get("created_by")
return row[0]
def _ensure_media_dirs():
sub = MEDIA_ROOT / "exercises"
sub.mkdir(parents=True, exist_ok=True)
return sub
def _detect_embed_platform(url: str) -> Optional[str]:
if not url:
return None
u = url.lower()
if "youtube.com" in u or "youtu.be" in u:
return "youtube"
if "vimeo.com" in u:
return "vimeo"
if "instagram.com" in u:
return "instagram"
if "tiktok.com" in u:
return "tiktok"
return None
def _assert_can_edit_exercise(cur, exercise_id: int, profile_id: int):
cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,))
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
if _row_created_by(row) != profile_id:
raise HTTPException(status_code=403, detail="Nur der Ersteller darf diese Übung bearbeiten")
def _count_exercise_media(cur, exercise_id: int) -> int:
cur.execute("SELECT COUNT(*) AS c FROM exercise_media WHERE exercise_id = %s", (exercise_id,))
r = cur.fetchone()
return int(r["c"] if isinstance(r, dict) else r[0])
def _abs_media_path(file_path_db: str) -> Optional[Path]:
if not file_path_db or file_path_db.startswith("http"):
return None
rel = file_path_db.lstrip("/")
if rel.startswith("media/"):
rel = rel[len("media/") :]
p = MEDIA_ROOT / rel
try:
p.resolve().relative_to(MEDIA_ROOT.resolve())
except ValueError:
return None
return p
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
def enrich_exercise_detail(exercise_id: int, cur) -> dict:
"""
Lädt alle M:N Relations für eine Übung und gibt ein vollständiges
Exercise-Objekt zurück (wie in API-Spec GET /exercises/{id}).
"""
# Basis-Exercise
cur.execute(
"""SELECT e.*, p.name as creator_name, c.name as club_name
FROM exercises e
LEFT JOIN profiles p ON e.created_by = p.id
LEFT JOIN clubs c ON e.club_id = c.id
WHERE e.id = %s""",
(exercise_id,)
)
row = cur.fetchone()
if not row:
return None
exercise = r2d(row)
# Equipment JSONB → List
if exercise.get("equipment"):
exercise["equipment"] = exercise["equipment"] if isinstance(exercise["equipment"], list) else []
else:
exercise["equipment"] = []
# Focus Areas (M:N)
cur.execute(
"""SELECT efa.id, efa.focus_area_id, fa.name, fa.abbreviation, fa.color, fa.icon, efa.is_primary
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 (M:N)
cur.execute(
"""SELECT ets.id, ets.style_direction_id as training_style_id, sd.name, sd.abbreviation, ets.is_primary
FROM exercise_style_directions ets
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
JOIN style_directions sd ON ets.style_direction_id = sd.id
WHERE ets.exercise_id = %s
ORDER BY ets.is_primary DESC, sd.name""",
(exercise_id,)
)
exercise["training_styles"] = [r2d(r) for r in cur.fetchall()]
# Trainingsstil (Breitensport / Leistungssport …) — exercise_training_types
cur.execute(
"""SELECT ett.id, ett.training_type_id, tt.name, tt.abbreviation, ett.is_primary
FROM exercise_training_types ett
JOIN training_types tt ON ett.training_type_id = tt.id
WHERE ett.exercise_id = %s
ORDER BY ett.is_primary DESC, tt.sort_order NULLS LAST, tt.name""",
(exercise_id,),
)
exercise["training_types"] = [r2d(r) for r in cur.fetchall()]
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Target Groups (M:N)
cur.execute(
"""SELECT etg.id, etg.target_group_id, tg.name, tg.description, etg.is_primary
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 (M:N) - direkt als VARCHAR gespeichert
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
cur.execute(
"""SELECT age_group
FROM exercise_age_groups
WHERE exercise_id = %s
ORDER BY age_group""",
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
(exercise_id,)
)
exercise["age_groups"] = [r["age_group"] for r in cur.fetchall()]
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Skills (M:N) mit Levels und Intensity
cur.execute(
"""SELECT es.id, es.skill_id, s.name as skill_name, s.category as skill_category,
es.is_primary, es.intensity, es.required_level, es.target_level, es.ai_suggested
FROM exercise_skills es
JOIN skills s ON es.skill_id = s.id
WHERE es.exercise_id = %s
ORDER BY es.is_primary DESC, s.name""",
(exercise_id,)
)
exercise["skills"] = [r2d(r) for r in cur.fetchall()]
# Variants (1:N) - mit Progression
cur.execute(
"""SELECT id, variant_name, description, execution_changes,
duration_min, duration_max, equipment_changes, difficulty_adjustment,
progression_level, sequence_order, prerequisite_variant_id
FROM exercise_variants
WHERE exercise_id = %s
ORDER BY progression_level, sequence_order""",
(exercise_id,)
)
exercise["variants"] = [r2d(r) for r in cur.fetchall()]
# Media (1:N)
cur.execute(
"""SELECT id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, sort_order, is_primary, context
FROM exercise_media
WHERE exercise_id = %s
ORDER BY sort_order, id""",
(exercise_id,)
)
exercise["media"] = [r2d(r) for r in cur.fetchall()]
return exercise
def assign_exercise_relations(cur, conn, exercise_id: int, data: dict):
"""
Weist M:N Relations für eine Übung zu.
Löscht alte Zuordnungen und legt neue an (REPLACE-Logik).
"""
# 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 (Stilrichtungen, z. B. Shotokan)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
if "training_styles_multi" in data:
cur.execute("DELETE FROM exercise_style_directions WHERE exercise_id = %s", (exercise_id,))
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
for ts in data["training_styles_multi"]:
cur.execute(
"""INSERT INTO exercise_style_directions (exercise_id, style_direction_id, is_primary)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
VALUES (%s, %s, %s)""",
(exercise_id, ts["training_style_id"], ts.get("is_primary", False))
)
# Trainingsstil (Breitensport, Leistungssport, …)
if "training_types_multi" in data:
cur.execute("DELETE FROM exercise_training_types WHERE exercise_id = %s", (exercise_id,))
for tt in data["training_types_multi"]:
cur.execute(
"""INSERT INTO exercise_training_types (exercise_id, training_type_id, is_primary)
VALUES (%s, %s, %s)""",
(exercise_id, tt["training_type_id"], tt.get("is_primary", False)),
)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# 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 (direkt als VARCHAR, CHECK constraint validiert)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
if "age_groups" in data:
cur.execute("DELETE FROM exercise_age_groups WHERE exercise_id = %s", (exercise_id,))
for age_group_name in data["age_groups"]:
try:
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
cur.execute(
"INSERT INTO exercise_age_groups (exercise_id, age_group) VALUES (%s, %s)",
(exercise_id, age_group_name)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
)
except Exception as e:
logger.warning("Age Group '%s' ungültig: %s", age_group_name, e)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Skills
if "skills" in data:
cur.execute("DELETE FROM exercise_skills WHERE exercise_id = %s", (exercise_id,))
for skill in data["skills"]:
cur.execute(
"""INSERT INTO exercise_skills
(exercise_id, skill_id, is_primary, intensity, required_level, target_level, ai_suggested)
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
(
exercise_id,
skill["skill_id"],
skill.get("is_primary", False),
skill.get("intensity"),
skill.get("required_level"),
skill.get("target_level"),
skill.get("ai_suggested", False),
)
)
conn.commit()
# ============================================================================
# Endpoints
# ============================================================================
@router.get("/exercises")
def list_exercises(
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
focus_area: Optional[int] = Query(default=None),
visibility: Optional[str] = Query(default=None),
status: Optional[str] = Query(default=None),
skill_id: Optional[int] = Query(default=None),
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
search: Optional[str] = Query(default=None),
limit: int = Query(default=50, ge=1, le=100),
offset: int = Query(default=0, ge=0),
session: dict = Depends(require_auth),
):
"""
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
Liste aller Übungen mit Filtern.
Lightweight Response (ohne M:N Details, nur IDs und Namen).
"""
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
profile_id = session["profile_id"]
with get_db() as conn:
cur = get_cursor(conn)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# WHERE-Bedingungen
where = ["1=1"]
params = []
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Visibility Filter (private nur für Owner)
where.append("(e.visibility = 'official' OR e.visibility = 'club' OR e.created_by = %s)")
params.append(profile_id)
if visibility:
where.append("e.visibility = %s")
params.append(visibility)
if status:
where.append("e.status = %s")
params.append(status)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Focus Area Filter (M:N Join)
if focus_area:
where.append("EXISTS (SELECT 1 FROM exercise_focus_areas efa WHERE efa.exercise_id = e.id AND efa.focus_area_id = %s)")
params.append(focus_area)
# Skill Filter (M:N Join)
if skill_id:
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
where.append("EXISTS (SELECT 1 FROM exercise_skills es WHERE es.exercise_id = e.id AND es.skill_id = %s)")
params.append(skill_id)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Volltext-Suche (tsvector)
if search:
where.append("e.search_vector @@ plainto_tsquery('german', %s)")
params.append(search)
# Query (primary_focus_name für Listen-Ansicht gemäß Spec-Beispiel „focus_area“-Label)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
query = f"""
SELECT e.id, e.title, e.summary, e.visibility, e.status,
e.created_by, p.name as creator_name,
e.club_id, c.name as club_name,
e.created_at, e.updated_at,
(
SELECT fa.name FROM exercise_focus_areas efa
JOIN focus_areas fa ON fa.id = efa.focus_area_id
WHERE efa.exercise_id = e.id
ORDER BY efa.is_primary DESC NULLS LAST, fa.name ASC
LIMIT 1
) AS primary_focus_name
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
FROM exercises e
LEFT JOIN profiles p ON e.created_by = p.id
LEFT JOIN clubs c ON e.club_id = c.id
WHERE {' AND '.join(where)}
ORDER BY e.updated_at DESC
LIMIT %s OFFSET %s
"""
params.extend([limit, offset])
cur.execute(query, params)
rows = cur.fetchall()
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
out = []
for r in rows:
d = r2d(r)
pfn = d.get("primary_focus_name")
d["focus_area"] = pfn
out.append(d)
return out
@router.get("/exercises/{exercise_id}")
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
def get_exercise(
exercise_id: int,
session: dict = Depends(require_auth),
):
"""
Exercise Detail mit allen M:N Relations (vollständig enriched).
"""
profile_id = session["profile_id"]
with get_db() as conn:
cur = get_cursor(conn)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
exercise = enrich_exercise_detail(exercise_id, cur)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
if not exercise:
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
# Permission Check (private nur für Owner)
if exercise["visibility"] == "private" and exercise["created_by"] != profile_id:
raise HTTPException(status_code=403, detail="Keine Berechtigung für diese Übung")
return exercise
@router.post("/exercises", status_code=201)
def create_exercise(
body: ExerciseCreate,
session: dict = Depends(require_auth),
):
"""
Erstellt eine neue Übung mit allen M:N Relations.
"""
profile_id = session["profile_id"]
# Validierung
if body.status not in ("draft", "in_review", "approved", "archived"):
raise HTTPException(status_code=400, detail="Ungültiger Status")
if body.visibility not in ("private", "club", "official"):
raise HTTPException(status_code=400, detail="Ungültige Visibility")
with get_db() as conn:
cur = get_cursor(conn)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Equipment als JSONB
equipment_json = json.dumps(body.equipment) if body.equipment else None
# INSERT
cur.execute(
"""INSERT INTO exercises
(title, summary, goal, execution, preparation, trainer_notes,
duration_min, duration_max, group_size_min, group_size_max,
equipment, visibility, status, created_by, club_id)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING id""",
(
body.title, body.summary, body.goal, body.execution,
body.preparation, body.trainer_notes,
body.duration_min, body.duration_max,
body.group_size_min, body.group_size_max,
equipment_json,
body.visibility, body.status, profile_id, body.club_id,
)
)
row = cur.fetchone()
exercise_id = row['id'] if isinstance(row, dict) else row[0]
conn.commit()
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# M:N Relations zuweisen
data = body.dict()
assign_exercise_relations(cur, conn, exercise_id, data)
# Vollständiges Objekt zurückgeben
exercise = enrich_exercise_detail(exercise_id, cur)
return exercise
@router.put("/exercises/{exercise_id}")
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
def update_exercise(
exercise_id: int,
body: ExerciseUpdate,
session: dict = Depends(require_auth),
):
"""
Aktualisiert eine Übung (Partial Update).
Nur Owner darf editieren.
"""
profile_id = session["profile_id"]
with get_db() as conn:
cur = get_cursor(conn)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Existiert die Übung?
cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,))
row = cur.fetchone()
if not row:
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Permission Check
if _row_created_by(row) != profile_id:
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
raise HTTPException(status_code=403, detail="Nur der Ersteller darf editieren")
# UPDATE (nur gesetzte Felder)
fields = []
params = []
data = body.dict(exclude_unset=True)
# Basis-Felder
for field in ["title", "summary", "goal", "execution", "preparation", "trainer_notes",
"duration_min", "duration_max", "group_size_min", "group_size_max",
"visibility", "status", "club_id"]:
if field in data and data[field] is not None:
fields.append(f"{field} = %s")
params.append(data[field])
# Equipment (JSONB)
if "equipment" in data:
fields.append("equipment = %s")
params.append(json.dumps(data["equipment"]) if data["equipment"] else None)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# UPDATE ausführen (wenn Basis-Felder geändert wurden)
if fields:
fields.append("updated_at = NOW()")
params.append(exercise_id)
query = f"UPDATE exercises SET {', '.join(fields)} WHERE id = %s"
cur.execute(query, params)
conn.commit()
# M:N Relations aktualisieren (wenn angegeben)
assign_exercise_relations(cur, conn, exercise_id, data)
# Vollständiges Objekt zurückgeben
exercise = enrich_exercise_detail(exercise_id, cur)
return exercise
@router.delete("/exercises/{exercise_id}")
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
def delete_exercise(
exercise_id: int,
session: dict = Depends(require_auth),
):
"""
Löscht eine Übung.
Nur Owner oder Admin darf löschen.
"""
profile_id = session["profile_id"]
role = session.get("role")
with get_db() as conn:
cur = get_cursor(conn)
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
# Existiert die Übung?
cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,))
row = cur.fetchone()
if not row:
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
raise HTTPException(status_code=404, detail="Übung nicht gefunden")
# Permission Check
if _row_created_by(row) != profile_id and role not in ("admin", "superadmin"):
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
raise HTTPException(status_code=403, detail="Nur Ersteller oder Admin darf löschen")
# Prüfen ob Übung in Block-Items verwendet wird
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
cur.execute(
"SELECT COUNT(*) AS cnt FROM exercise_block_items WHERE exercise_id = %s",
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
(exercise_id,)
)
crow = cur.fetchone()
count = crow["cnt"] if isinstance(crow, dict) else crow[0]
feat: Exercises v2.0 + Migrations 014/016/017 (Clean-Room Rebuild) BREAKING CHANGES: - exercises.py komplett neu gebaut (kein Legacy-Code) - Legacy-Felder entfernt: age_groups, focus_area, secondary_areas, training_character - Nur M:N Relations, keine JSONB-Kataloge Migrations: - Migration 014: Variant Progression + Search Vector + Legacy DROP - exercise_variants: progression_level, sequence_order, prerequisite_variant_id - exercises: search_vector (tsvector für Volltext-Suche) - DROP age_groups, focus_area, secondary_areas, training_character - Helper: update_timestamp() Funktion für Triggers - Migration 016: Saved Exercise Searches - saved_exercise_searches (profile_id, name, filters JSONB) - Migration 017: Exercise Blocks + Template Blocks - exercise_blocks (name, description, goal, is_template) - exercise_block_items (exercise_id, variant_id, sequence_order, is_placeholder, placeholder_criteria) Backend (exercises.py v2.0): - GET /exercises: Volltext-Suche via tsvector, M:N Joins - GET /exercises/{id}: enrich_exercise_detail() mit allen M:N Relations - POST /exercises: M:N Relations (focus_areas_multi, training_styles_multi, target_groups_multi, age_groups, skills) - PUT /exercises: Partial Update + M:N Relations - DELETE /exercises: Cascade-Check für exercise_block_items Architecture: - Issue #53 konform: Import = Feld-Zuordnung, keine fachliche Interpretation - Helper: enrich_exercise_detail() für vollständige Objekte - Helper: assign_exercise_relations() für M:N Management (DELETE+INSERT Pattern) Docs: - SMW_IMPORTER_GAP_ANALYSIS.md: Vollständige Gap-Analyse + Umsetzungsplan Version: 0.7.0 Module: exercises 2.0.0 Schema: 20260424002
2026-04-24 15:04:27 +02:00
if count > 0:
raise HTTPException(
status_code=409,
detail=f"Übung wird in {count} Block-Item(s) verwendet und kann nicht gelöscht werden"
)
# DELETE (Cascade löscht M:N Zuordnungen automatisch)
cur.execute("DELETE FROM exercises WHERE id = %s", (exercise_id,))
conn.commit()
return {"ok": True}
# --- Medien (MEDIA_UPLOAD_SPEC.md / EXERCISES_API_SPEC.md) ---
def _fetch_media_row(cur, exercise_id: int, media_id: int) -> Optional[dict]:
cur.execute(
"""SELECT id, exercise_id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, sort_order, is_primary, context, created_at
FROM exercise_media WHERE id = %s AND exercise_id = %s""",
(media_id, exercise_id),
)
row = cur.fetchone()
return r2d(row) if row else None
@router.post("/exercises/{exercise_id}/media", status_code=201)
async def upload_exercise_media(
exercise_id: int,
session: dict = Depends(require_auth),
file: Optional[UploadFile] = File(None),
embed_url: Optional[str] = Form(None),
media_type: str = Form(...),
title: str = Form(""),
description: str = Form(""),
context: str = Form("ablauf"),
is_primary: bool = Form(False),
):
profile_id = session["profile_id"]
if media_type not in ("image", "video", "document", "sketch"):
raise HTTPException(status_code=400, detail="Ungültiger media_type")
if context not in ("ablauf", "detail", "trainer_hint"):
raise HTTPException(status_code=400, detail="Ungültiger context")
emb = (embed_url or "").strip() or None
has_file = file is not None and bool(file.filename)
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, profile_id)
if _count_exercise_media(cur, exercise_id) >= MAX_EXERCISE_MEDIA:
raise HTTPException(
status_code=400,
detail=f"Maximal {MAX_EXERCISE_MEDIA} Medien pro Übung",
)
if has_file and emb:
raise HTTPException(status_code=400, detail="Entweder Datei oder embed_url, nicht beides")
if not has_file and not emb:
raise HTTPException(status_code=400, detail="Datei oder embed_url erforderlich")
sort_sql = (
"COALESCE((SELECT MAX(sort_order) + 1 FROM exercise_media WHERE exercise_id = %s), 1)"
)
if emb:
platform = _detect_embed_platform(emb)
if not platform:
raise HTTPException(
status_code=400,
detail="Ungültige Embed-URL (erlaubt: YouTube, Vimeo, Instagram, TikTok)",
)
cur.execute(
f"""INSERT INTO exercise_media (
exercise_id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, context, is_primary, sort_order
) VALUES (
%s, %s, NULL, NULL, NULL, NULL, %s, %s, %s, %s, %s, %s, {sort_sql}
)
RETURNING id, exercise_id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, sort_order, is_primary, context, created_at""",
(
exercise_id,
media_type,
emb,
platform,
title or None,
description or None,
context,
is_primary,
exercise_id,
),
)
else:
raw = await file.read()
if len(raw) > MAX_UPLOAD_BYTES:
raise HTTPException(
status_code=413,
detail=f"Datei zu groß (max. {MAX_UPLOAD_BYTES // (1024 * 1024)} MB)",
)
mime = file.content_type or ""
if mime not in ALLOWED_UPLOAD_MIMES:
raise HTTPException(
status_code=400,
detail=f"Dateityp nicht erlaubt: {mime or 'unbekannt'}",
)
ext = Path(file.filename or "").suffix[:12] if file.filename else ""
if not ext and mime == "image/jpeg":
ext = ".jpg"
elif not ext and mime == "image/png":
ext = ".png"
digest = hashlib.sha256(raw).hexdigest()[:12]
fname = f"{digest}_{exercise_id}{ext}"
dest_dir = _ensure_media_dirs()
dest_path = dest_dir / fname
dest_path.write_bytes(raw)
db_path = f"/media/exercises/{fname}"
cur.execute(
f"""INSERT INTO exercise_media (
exercise_id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, context, is_primary, sort_order
) VALUES (
%s, %s, %s, %s, %s, %s, NULL, NULL, %s, %s, %s, %s, {sort_sql}
)
RETURNING id, exercise_id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, sort_order, is_primary, context, created_at""",
(
exercise_id,
media_type,
db_path,
len(raw),
mime,
file.filename,
title or None,
description or None,
context,
is_primary,
exercise_id,
),
)
row = cur.fetchone()
conn.commit()
return r2d(row)
@router.put("/exercises/{exercise_id}/media/reorder")
def reorder_exercise_media(
exercise_id: int,
body: ExerciseMediaReorder,
session: dict = Depends(require_auth),
):
profile_id = session["profile_id"]
ids = body.media_ids
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, profile_id)
cur.execute(
"SELECT id FROM exercise_media WHERE exercise_id = %s ORDER BY sort_order, id",
(exercise_id,),
)
existing = [r["id"] if isinstance(r, dict) else r[0] for r in cur.fetchall()]
if set(ids) != set(existing) or len(ids) != len(existing):
raise HTTPException(
status_code=400,
detail="media_ids unvollständig oder gehören nicht zu dieser Übung",
)
for i, mid in enumerate(ids, start=1):
cur.execute(
"UPDATE exercise_media SET sort_order = %s, updated_at = NOW() WHERE id = %s AND exercise_id = %s",
(i, mid, exercise_id),
)
conn.commit()
return {"ok": True, "reordered": len(ids)}
@router.put("/exercises/{exercise_id}/media/{media_id}")
def update_exercise_media(
exercise_id: int,
media_id: int,
body: ExerciseMediaUpdate,
session: dict = Depends(require_auth),
):
profile_id = session["profile_id"]
data = body.dict(exclude_unset=True)
if not data:
raise HTTPException(status_code=400, detail="Keine Felder zum Aktualisieren")
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, profile_id)
if not _fetch_media_row(cur, exercise_id, media_id):
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
if "context" in data and data["context"] not in ("ablauf", "detail", "trainer_hint", None):
raise HTTPException(status_code=400, detail="Ungültiger context")
fields = []
params = []
for k in ("title", "description", "is_primary", "context"):
if k in data:
fields.append(f"{k} = %s")
params.append(data[k])
if fields:
fields.append("updated_at = NOW()")
params.extend([media_id, exercise_id])
cur.execute(
f"UPDATE exercise_media SET {', '.join(fields)} WHERE id = %s AND exercise_id = %s",
params,
)
conn.commit()
cur.execute(
"""SELECT id, media_type, file_path, file_size, mime_type, original_filename,
embed_url, embed_platform, title, description, sort_order, is_primary, context, created_at
FROM exercise_media WHERE id = %s""",
(media_id,),
)
return r2d(cur.fetchone())
@router.delete("/exercises/{exercise_id}/media/{media_id}")
def delete_exercise_media(
exercise_id: int,
media_id: int,
session: dict = Depends(require_auth),
):
profile_id = session["profile_id"]
with get_db() as conn:
cur = get_cursor(conn)
_assert_can_edit_exercise(cur, exercise_id, profile_id)
cur.execute(
"""SELECT file_path FROM exercise_media WHERE id = %s AND exercise_id = %s""",
(media_id, exercise_id),
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="Medium nicht gefunden")
fp = row["file_path"] if isinstance(row, dict) else row[0]
cur.execute(
"DELETE FROM exercise_media WHERE id = %s AND exercise_id = %s",
(media_id, exercise_id),
)
conn.commit()
abs_p = _abs_media_path(fp) if fp else None
if abs_p and abs_p.is_file():
try:
abs_p.unlink()
except OSError as e:
logger.warning("Medien-Datei konnte nicht gelöscht werden: %s", e)
return {"ok": True}