""" Exercise Management Endpoints for Shinkan Jinkendo Handles CRUD operations for exercises (Übungen). """ from typing import Optional from fastapi import APIRouter, HTTPException, Depends, Query from db import get_db, get_cursor, r2d from auth import require_auth router = APIRouter(prefix="/api", tags=["exercises"]) # ── List Exercises ──────────────────────────────────────────────────────── @router.get("/exercises") def list_exercises( focus_area: Optional[str] = Query(default=None), visibility: Optional[str] = Query(default=None), status: Optional[str] = Query(default=None), skill_id: Optional[int] = Query(default=None), session=Depends(require_auth) ): """ List exercises with optional filters. Filters: - focus_area: karate, selbstverteidigung, gewaltschutz - visibility: private, club, official - status: draft, in_review, approved, archived - skill_id: Filter by associated skill """ profile_id = session['profile_id'] with get_db() as conn: cur = get_cursor(conn) # Base query query = """ SELECT DISTINCT e.*, p.name as creator_name, c.name as club_name, tm.name as primary_method_name FROM exercises e LEFT JOIN profiles p ON e.created_by = p.id LEFT JOIN clubs c ON e.club_id = c.id LEFT JOIN training_methods tm ON e.primary_method_id = tm.id """ # Add skill join if filtering by skill if skill_id: query += " LEFT JOIN exercise_skills es ON e.id = es.exercise_id" # Build WHERE clause where = [] params = [] # Visibility filter: show own private + club + official where.append("(e.visibility = 'official' OR e.visibility = 'club' OR e.created_by = %s)") params.append(profile_id) if focus_area: where.append("e.focus_area = %s") params.append(focus_area) if visibility: where.append("e.visibility = %s") params.append(visibility) if status: where.append("e.status = %s") params.append(status) if skill_id: where.append("es.skill_id = %s") params.append(skill_id) if where: query += " WHERE " + " AND ".join(where) query += " ORDER BY e.created_at DESC" cur.execute(query, params) rows = cur.fetchall() return [r2d(r) for r in rows] # ── Get Exercise ────────────────────────────────────────────────────────── @router.get("/exercises/{exercise_id}") def get_exercise(exercise_id: int, session=Depends(require_auth)): """Get exercise by ID with associated skills and variants.""" profile_id = session['profile_id'] with get_db() as conn: cur = get_cursor(conn) # Get exercise cur.execute(""" SELECT e.*, p.name as creator_name, c.name as club_name, tm.name as primary_method_name FROM exercises e LEFT JOIN profiles p ON e.created_by = p.id LEFT JOIN clubs c ON e.club_id = c.id LEFT JOIN training_methods tm ON e.primary_method_id = tm.id WHERE e.id = %s """, (exercise_id,)) exercise = cur.fetchone() if not exercise: raise HTTPException(404, "Übung nicht gefunden") exercise = r2d(exercise) # Check visibility if exercise['visibility'] == 'private' and exercise['created_by'] != profile_id: raise HTTPException(403, "Keine Berechtigung") # Get associated skills cur.execute(""" SELECT es.*, s.name as skill_name, s.category as skill_category 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()] # Get variants cur.execute(""" SELECT * FROM exercise_variants WHERE exercise_id = %s ORDER BY created_at """, (exercise_id,)) exercise['variants'] = [r2d(r) for r in cur.fetchall()] # Get media cur.execute(""" SELECT * FROM exercise_media WHERE exercise_id = %s ORDER BY sort_order, created_at """, (exercise_id,)) exercise['media'] = [r2d(r) for r in cur.fetchall()] return exercise # ── Create Exercise ─────────────────────────────────────────────────────── @router.post("/exercises") def create_exercise(data: dict, session=Depends(require_auth)): """Create new exercise.""" profile_id = session['profile_id'] # Required fields title = data.get('title') goal = data.get('goal') execution = data.get('execution') if not title or not goal or not execution: raise HTTPException(400, "Titel, Ziel und Durchführung sind Pflichtfelder") with get_db() as conn: cur = get_cursor(conn) # Insert exercise cur.execute(""" INSERT INTO exercises ( title, summary, goal, execution, preparation, trainer_notes, equipment, duration_min, duration_max, group_size_min, group_size_max, age_groups, focus_area, secondary_areas, training_character, primary_method_id, secondary_method_ids, visibility, status, created_by, club_id ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s ) RETURNING id """, ( title, data.get('summary'), goal, execution, data.get('preparation'), data.get('trainer_notes'), data.get('equipment'), # JSONB data.get('duration_min'), data.get('duration_max'), data.get('group_size_min'), data.get('group_size_max'), data.get('age_groups'), # JSONB data.get('focus_area'), data.get('secondary_areas'), # JSONB data.get('training_character'), data.get('primary_method_id'), data.get('secondary_method_ids'), # JSONB data.get('visibility', 'private'), data.get('status', 'draft'), profile_id, data.get('club_id') )) exercise_id = cur.fetchone()['id'] # Add skills if provided if data.get('skills'): for skill in data['skills']: cur.execute(""" INSERT INTO exercise_skills ( exercise_id, skill_id, is_primary, intensity, development_contribution, required_level, target_level ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( exercise_id, skill['skill_id'], skill.get('is_primary', False), skill.get('intensity'), skill.get('development_contribution'), skill.get('required_level'), skill.get('target_level') )) conn.commit() return get_exercise(exercise_id, session) # ── Update Exercise ─────────────────────────────────────────────────────── @router.put("/exercises/{exercise_id}") def update_exercise(exercise_id: int, data: dict, session=Depends(require_auth)): """Update exercise.""" profile_id = session['profile_id'] with get_db() as conn: cur = get_cursor(conn) # Check ownership cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,)) row = cur.fetchone() if not row: raise HTTPException(404, "Übung nicht gefunden") if row['created_by'] != profile_id: raise HTTPException(403, "Keine Berechtigung") # Update exercise cur.execute(""" UPDATE exercises SET title = %s, summary = %s, goal = %s, execution = %s, preparation = %s, trainer_notes = %s, equipment = %s, duration_min = %s, duration_max = %s, group_size_min = %s, group_size_max = %s, age_groups = %s, focus_area = %s, secondary_areas = %s, training_character = %s, primary_method_id = %s, secondary_method_ids = %s, visibility = %s, status = %s, club_id = %s, updated_at = NOW() WHERE id = %s """, ( data.get('title'), data.get('summary'), data.get('goal'), data.get('execution'), data.get('preparation'), data.get('trainer_notes'), data.get('equipment'), data.get('duration_min'), data.get('duration_max'), data.get('group_size_min'), data.get('group_size_max'), data.get('age_groups'), data.get('focus_area'), data.get('secondary_areas'), data.get('training_character'), data.get('primary_method_id'), data.get('secondary_method_ids'), data.get('visibility'), data.get('status'), data.get('club_id'), exercise_id )) # Update skills if provided if 'skills' in data: # Delete existing skills cur.execute("DELETE FROM exercise_skills WHERE exercise_id = %s", (exercise_id,)) # Add new skills for skill in data['skills']: cur.execute(""" INSERT INTO exercise_skills ( exercise_id, skill_id, is_primary, intensity, development_contribution, required_level, target_level ) VALUES (%s, %s, %s, %s, %s, %s, %s) """, ( exercise_id, skill['skill_id'], skill.get('is_primary', False), skill.get('intensity'), skill.get('development_contribution'), skill.get('required_level'), skill.get('target_level') )) conn.commit() return get_exercise(exercise_id, session) # ── Delete Exercise ─────────────────────────────────────────────────────── @router.delete("/exercises/{exercise_id}") def delete_exercise(exercise_id: int, session=Depends(require_auth)): """Delete exercise (only owner or admin).""" profile_id = session['profile_id'] role = session.get('role') with get_db() as conn: cur = get_cursor(conn) # Check ownership or admin cur.execute("SELECT created_by FROM exercises WHERE id = %s", (exercise_id,)) row = cur.fetchone() if not row: raise HTTPException(404, "Übung nicht gefunden") if row['created_by'] != profile_id and role not in ['admin', 'superadmin']: raise HTTPException(403, "Keine Berechtigung") # Delete (CASCADE handles skills, variants, media) cur.execute("DELETE FROM exercises WHERE id = %s", (exercise_id,)) conn.commit() return {"ok": True}