import uuid from datetime import datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import func, or_ from sqlalchemy.orm import Session, joinedload import auth import database import models import schemas router = APIRouter( prefix="/exercises", tags=["exercises"], dependencies=[Depends(auth.get_current_user)], ) def get_current_timestamp() -> str: return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%fZ") def new_sync_id() -> str: return uuid.uuid4().hex def normalize_name(name: str) -> str: return (name or "").strip().lower() def find_exercise_by_name( db: Session, user: models.User, name: str, ) -> Optional[models.Exercise]: n = normalize_name(name) if not n: return None owned = ( db.query(models.Exercise) .filter( models.Exercise.owner_id == user.id, func.lower(models.Exercise.name) == n, ) .first() ) if owned: return owned return ( db.query(models.Exercise) .filter( models.Exercise.owner_id.is_(None), func.lower(models.Exercise.name) == n, ) .first() ) def get_owned_exercise( db: Session, exercise_id: int, user: models.User, ) -> models.Exercise: exercise = ( db.query(models.Exercise) .filter( models.Exercise.id == exercise_id, models.Exercise.owner_id == user.id, ) .first() ) if not exercise: raise HTTPException(status_code=404, detail="Exercise not found") return exercise @router.post("/", response_model=schemas.ExerciseOut) def create_exercise( exercise: schemas.ExerciseCreate, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): sync_id = exercise.sync_id or new_sync_id() timestamp = get_current_timestamp() existing = db.query(models.Exercise).filter(models.Exercise.sync_id == sync_id).first() if existing: if existing.owner_id is not None and existing.owner_id != current_user.id: raise HTTPException(status_code=409, detail="Exercise sync_id belongs to another user") if existing.owner_id == current_user.id: existing.name = exercise.name existing.workout_type_id = exercise.workout_type_id existing.updated_at = timestamp db.commit() db.refresh(existing) return existing # Глобальное упражнение — не даём пользователю его менять, просто возвращаем return existing by_name = find_exercise_by_name(db, current_user, exercise.name) if by_name: if by_name.owner_id == current_user.id: by_name.sync_id = by_name.sync_id or sync_id by_name.workout_type_id = exercise.workout_type_id by_name.updated_at = timestamp db.commit() db.refresh(by_name) return by_name # Нашли глобальное с таким же именем — используем его return by_name db_exercise = models.Exercise( sync_id=sync_id, owner_id=current_user.id, name=exercise.name.strip(), workout_type_id=exercise.workout_type_id, created_at=timestamp, updated_at=timestamp, ) db.add(db_exercise) db.commit() db.refresh(db_exercise) return db_exercise @router.get("/", response_model=List[schemas.ExerciseOut]) def list_exercises( workout_type_id: Optional[int] = None, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): query = db.query(models.Exercise).filter( or_( models.Exercise.owner_id == current_user.id, models.Exercise.owner_id.is_(None), ) ) if workout_type_id: query = query.filter(models.Exercise.workout_type_id == workout_type_id) return query.order_by(models.Exercise.name).all() @router.get("/history/{exercise_name}") def get_exercise_history( exercise_name: str, client_id: Optional[int] = None, limit: int = 10, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): """ Возвращает историю упражнения для текущего пользователя. Если указан client_id — только тренировки этого клиента. """ normalized = normalize_name(exercise_name) workout_query = ( db.query(models.Workout.id) .join(models.WorkoutExercise, models.WorkoutExercise.workout_id == models.Workout.id) .filter( models.Workout.owner_id == current_user.id, models.Workout.end_time.isnot(None), func.lower(models.WorkoutExercise.exercise_name) == normalized, ) ) if client_id: client = ( db.query(models.Client) .filter( models.Client.id == client_id, models.Client.owner_id == current_user.id, ) .first() ) if not client: raise HTTPException(status_code=404, detail="Client not found") workout_query = workout_query.filter(models.Workout.client_id == client_id) workout_ids = [ row[0] for row in workout_query.order_by(models.Workout.start_time.desc()).limit(limit).all() ] if not workout_ids: return [] workouts = ( db.query(models.Workout) .options( joinedload(models.Workout.workout_exercises) .joinedload(models.WorkoutExercise.sets) .joinedload(models.Set.dropset_items) ) .filter(models.Workout.id.in_(workout_ids)) .order_by(models.Workout.start_time.desc()) .all() ) result = [] for workout in workouts: for we in workout.workout_exercises or []: if normalize_name(we.exercise_name) != normalized: continue sets_payload = [] for s in we.sets or []: sets_payload.append( { "is_dropset": bool(s.is_dropset), "weight": s.weight, "reps": s.reps, "rating": s.rating, "notes": s.notes or "", "timestamp": s.timestamp, "dropset_items": [ { "weight": di.weight, "reps": di.reps, "sort_order": di.sort_order, } for di in (s.dropset_items or []) ], } ) if sets_payload: result.append( { "workout_sync_id": workout.sync_id, "date": workout.start_time, "client_id": workout.client_id, "exercise_name": we.exercise_name, "sets": sets_payload, } ) return result @router.get("/{exercise_id}", response_model=schemas.ExerciseOut) def get_exercise( exercise_id: int, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): exercise = ( db.query(models.Exercise) .filter( or_( models.Exercise.id == exercise_id, ), or_( models.Exercise.owner_id == current_user.id, models.Exercise.owner_id.is_(None), ), ) .first() ) if not exercise: raise HTTPException(status_code=404, detail="Exercise not found") return exercise @router.put("/{exercise_id}", response_model=schemas.ExerciseOut) def update_exercise( exercise_id: int, exercise: schemas.ExerciseUpdate, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): db_exercise = get_owned_exercise(db, exercise_id, current_user) update_data = exercise.dict(exclude_unset=True) for key, value in update_data.items(): setattr(db_exercise, key, value) db_exercise.updated_at = get_current_timestamp() db.commit() db.refresh(db_exercise) return db_exercise @router.delete("/{exercise_id}") def delete_exercise( exercise_id: int, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(database.get_db), ): db_exercise = get_owned_exercise(db, exercise_id, current_user) db.delete(db_exercise) db.commit() return {"status": "deleted"}