Codebase hardening: 102 fixes across 35+ files
Deep audit identified 106 findings; 102 fixed, 4 deferred. Covers 8 areas: - Settings & deploy: env-gated DEBUG/SECRET_KEY, HTTPS headers, gunicorn, celery worker - Auth (registered_user): password write_only, request.data fixes, transaction safety, proper HTTP status codes - Workout app: IDOR protection, get_object_or_404, prefetch_related N+1 fixes, transaction.atomic - Video/scripts: path traversal sanitization, HLS trigger guard, auth on cache wipe - Models (exercise/equipment/muscle/superset): null-safe __str__, stable IDs, prefetch support - Generator views: helper for registered_user lookup, logger.exception, bulk_update, transaction wrapping - Generator core (rules/selector/generator): push-pull ratio, type affinity normalization, modality checks, side-pair exact match, word-boundary regex, equipment cache clearing - Generator services (plan_builder/analyzer/normalizer): transaction.atomic, muscle cache, bulk_update, glutes classification fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import (
|
||||
api_view,
|
||||
@@ -44,6 +46,19 @@ from .serializers import (
|
||||
)
|
||||
from exercise.serializers import ExerciseSerializer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_registered_user(request):
|
||||
"""Get RegisteredUser for the authenticated user, or 404.
|
||||
|
||||
Caches the result on the request object to avoid repeated DB hits
|
||||
when called multiple times in the same request cycle.
|
||||
"""
|
||||
if not hasattr(request, '_registered_user'):
|
||||
request._registered_user = get_object_or_404(RegisteredUser, user=request.user)
|
||||
return request._registered_user
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Generation Rules
|
||||
@@ -67,7 +82,7 @@ def generation_rules(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_preferences(request):
|
||||
"""Get (or auto-create) the UserPreference for the logged-in user."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
preference, _created = UserPreference.objects.get_or_create(
|
||||
registered_user=registered_user,
|
||||
)
|
||||
@@ -80,7 +95,7 @@ def get_preferences(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def update_preferences(request):
|
||||
"""Update the logged-in user's preferences. Accepts equipment_ids, muscle_ids, workout_type_ids."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
preference, _created = UserPreference.objects.get_or_create(
|
||||
registered_user=registered_user,
|
||||
)
|
||||
@@ -109,7 +124,7 @@ def generate_plan(request):
|
||||
Generate a weekly workout plan.
|
||||
Body: {"week_start_date": "YYYY-MM-DD"}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
|
||||
week_start_date_str = request.data.get('week_start_date')
|
||||
if not week_start_date_str:
|
||||
@@ -191,8 +206,9 @@ def generate_plan(request):
|
||||
generation_warnings = generator.warnings
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in generate_plan")
|
||||
return Response(
|
||||
{'error': f'Plan generation failed: {str(e)}'},
|
||||
{"error": "An unexpected error occurred. Please try again."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -212,9 +228,11 @@ def generate_plan(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def list_plans(request):
|
||||
"""List all generated plans for the logged-in user, newest first."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
plans = GeneratedWeeklyPlan.objects.filter(
|
||||
registered_user=registered_user,
|
||||
).select_related(
|
||||
'registered_user',
|
||||
).prefetch_related(
|
||||
'generated_workouts__workout_type',
|
||||
'generated_workouts__workout',
|
||||
@@ -228,9 +246,11 @@ def list_plans(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def plan_detail(request, plan_id):
|
||||
"""Get a single plan with all its generated workouts."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
plan = get_object_or_404(
|
||||
GeneratedWeeklyPlan.objects.prefetch_related(
|
||||
GeneratedWeeklyPlan.objects.select_related(
|
||||
'registered_user',
|
||||
).prefetch_related(
|
||||
'generated_workouts__workout_type',
|
||||
'generated_workouts__workout',
|
||||
),
|
||||
@@ -253,9 +273,9 @@ def accept_workout(request, workout_id):
|
||||
Accept a generated workout.
|
||||
Sets status to 'accepted' and creates a PlannedWorkout for the scheduled_date.
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
generated_workout = get_object_or_404(
|
||||
GeneratedWorkout,
|
||||
GeneratedWorkout.objects.select_related('workout', 'workout_type'),
|
||||
pk=workout_id,
|
||||
plan__registered_user=registered_user,
|
||||
)
|
||||
@@ -298,9 +318,9 @@ def reject_workout(request, workout_id):
|
||||
Reject a generated workout with optional feedback.
|
||||
Body: {"feedback": "..."}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
generated_workout = get_object_or_404(
|
||||
GeneratedWorkout,
|
||||
GeneratedWorkout.objects.select_related('workout', 'workout_type'),
|
||||
pk=workout_id,
|
||||
plan__registered_user=registered_user,
|
||||
)
|
||||
@@ -328,9 +348,9 @@ def rate_workout(request, workout_id):
|
||||
Rate a generated workout 1-5 with optional feedback.
|
||||
Body: {"rating": 5, "feedback": "..."}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
generated_workout = get_object_or_404(
|
||||
GeneratedWorkout,
|
||||
GeneratedWorkout.objects.select_related('workout', 'workout_type'),
|
||||
pk=workout_id,
|
||||
plan__registered_user=registered_user,
|
||||
)
|
||||
@@ -379,9 +399,9 @@ def regenerate_workout(request, workout_id):
|
||||
Regenerate a single workout within an existing plan.
|
||||
Deletes the old linked Workout (if any) and generates a fresh one for the same day/type.
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
generated_workout = get_object_or_404(
|
||||
GeneratedWorkout,
|
||||
GeneratedWorkout.objects.select_related('workout', 'workout_type', 'plan'),
|
||||
pk=workout_id,
|
||||
plan__registered_user=registered_user,
|
||||
)
|
||||
@@ -413,20 +433,15 @@ def regenerate_workout(request, workout_id):
|
||||
|
||||
generator = WorkoutGenerator(preference)
|
||||
|
||||
# Exclude exercises from sibling workouts in the same plan (Item #9)
|
||||
sibling_workouts = GeneratedWorkout.objects.filter(
|
||||
plan=generated_workout.plan,
|
||||
is_rest_day=False,
|
||||
workout__isnull=False,
|
||||
).exclude(pk=generated_workout.pk)
|
||||
sibling_exercise_ids = set()
|
||||
for sibling in sibling_workouts:
|
||||
if sibling.workout:
|
||||
sibling_exercise_ids.update(
|
||||
SupersetExercise.objects.filter(
|
||||
superset__workout=sibling.workout
|
||||
).values_list('exercise_id', flat=True)
|
||||
)
|
||||
# Exclude exercises from sibling workouts in the same plan (single query)
|
||||
sibling_exercise_ids = set(
|
||||
SupersetExercise.objects.filter(
|
||||
superset__workout__generated_from__plan=generated_workout.plan,
|
||||
superset__workout__generated_from__is_rest_day=False,
|
||||
).exclude(
|
||||
superset__workout__generated_from=generated_workout,
|
||||
).values_list('exercise_id', flat=True)
|
||||
)
|
||||
if sibling_exercise_ids:
|
||||
generator.exercise_selector.hard_exclude_ids.update(sibling_exercise_ids)
|
||||
|
||||
@@ -489,8 +504,9 @@ def regenerate_workout(request, workout_id):
|
||||
cache.delete(f"plan{generated_workout.plan_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in regenerate_workout")
|
||||
return Response(
|
||||
{'error': f'Regeneration failed: {str(e)}'},
|
||||
{"error": "An unexpected error occurred. Please try again."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -510,9 +526,9 @@ def delete_workout_day(request, workout_id):
|
||||
Delete a generated workout day (converts it to a rest day).
|
||||
Deletes the linked Workout object (cascading to supersets/exercises).
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
generated_workout = get_object_or_404(
|
||||
GeneratedWorkout,
|
||||
GeneratedWorkout.objects.select_related('workout'),
|
||||
pk=workout_id,
|
||||
plan__registered_user=registered_user,
|
||||
)
|
||||
@@ -545,7 +561,7 @@ def delete_workout_day(request, workout_id):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def delete_superset(request, superset_id):
|
||||
"""Delete a superset from a workout. Re-orders remaining supersets."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
superset = get_object_or_404(Superset, pk=superset_id)
|
||||
|
||||
# Verify ownership through the workout
|
||||
@@ -565,11 +581,14 @@ def delete_superset(request, superset_id):
|
||||
# Invalidate workout detail cache
|
||||
cache.delete(f"wk{workout.id}")
|
||||
|
||||
# Re-order remaining supersets
|
||||
remaining = Superset.objects.filter(workout=workout, order__gt=deleted_order).order_by('order')
|
||||
# Re-order remaining supersets with bulk_update
|
||||
remaining = list(
|
||||
Superset.objects.filter(workout=workout, order__gt=deleted_order).order_by('order')
|
||||
)
|
||||
for ss in remaining:
|
||||
ss.order -= 1
|
||||
ss.save()
|
||||
if remaining:
|
||||
Superset.objects.bulk_update(remaining, ['order'])
|
||||
|
||||
return Response({'status': 'deleted'}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -579,7 +598,7 @@ def delete_superset(request, superset_id):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def delete_superset_exercise(request, exercise_id):
|
||||
"""Delete an exercise from a superset. Re-orders remaining exercises."""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
superset_exercise = get_object_or_404(SupersetExercise, pk=exercise_id)
|
||||
|
||||
# Verify ownership
|
||||
@@ -600,11 +619,14 @@ def delete_superset_exercise(request, exercise_id):
|
||||
# Invalidate workout detail cache
|
||||
cache.delete(f"wk{workout.id}")
|
||||
|
||||
# Re-order remaining exercises
|
||||
remaining = SupersetExercise.objects.filter(superset=superset, order__gt=deleted_order).order_by('order')
|
||||
# Re-order remaining exercises with bulk_update
|
||||
remaining = list(
|
||||
SupersetExercise.objects.filter(superset=superset, order__gt=deleted_order).order_by('order')
|
||||
)
|
||||
for se in remaining:
|
||||
se.order -= 1
|
||||
se.save()
|
||||
if remaining:
|
||||
SupersetExercise.objects.bulk_update(remaining, ['order'])
|
||||
|
||||
# If the superset is now empty, delete it too
|
||||
if SupersetExercise.objects.filter(superset=superset).count() == 0:
|
||||
@@ -653,7 +675,7 @@ def swap_exercise(request, exercise_id):
|
||||
Swap a SupersetExercise's exercise for a new one.
|
||||
Body: {"new_exercise_id": 123}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
superset_exercise = get_object_or_404(SupersetExercise, pk=exercise_id)
|
||||
|
||||
# Verify ownership
|
||||
@@ -734,7 +756,7 @@ def analysis_stats(request):
|
||||
"""
|
||||
muscle_splits = MuscleGroupSplit.objects.all()
|
||||
weekly_patterns = WeeklySplitPattern.objects.all()
|
||||
structure_rules = WorkoutStructureRule.objects.all()
|
||||
structure_rules = WorkoutStructureRule.objects.select_related('workout_type').all()
|
||||
movement_orders = MovementPatternOrder.objects.all()
|
||||
|
||||
data = {
|
||||
@@ -778,29 +800,35 @@ def confirm_plan(request, plan_id):
|
||||
"""
|
||||
Batch-accept all workouts in a plan and create PlannedWorkout entries.
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
plan = get_object_or_404(
|
||||
GeneratedWeeklyPlan,
|
||||
pk=plan_id,
|
||||
registered_user=registered_user,
|
||||
)
|
||||
|
||||
workouts = GeneratedWorkout.objects.filter(plan=plan)
|
||||
for gw in workouts:
|
||||
if gw.is_rest_day or not gw.workout:
|
||||
continue
|
||||
gw.status = 'accepted'
|
||||
gw.save()
|
||||
workouts = GeneratedWorkout.objects.filter(plan=plan).select_related('workout')
|
||||
|
||||
PlannedWorkout.objects.filter(
|
||||
registered_user=registered_user,
|
||||
on_date=gw.scheduled_date,
|
||||
).delete()
|
||||
PlannedWorkout.objects.create(
|
||||
workout=gw.workout,
|
||||
registered_user=registered_user,
|
||||
on_date=gw.scheduled_date,
|
||||
)
|
||||
with transaction.atomic():
|
||||
workouts_to_update = []
|
||||
for gw in workouts:
|
||||
if gw.is_rest_day or not gw.workout:
|
||||
continue
|
||||
gw.status = 'accepted'
|
||||
workouts_to_update.append(gw)
|
||||
|
||||
PlannedWorkout.objects.filter(
|
||||
registered_user=registered_user,
|
||||
on_date=gw.scheduled_date,
|
||||
).delete()
|
||||
PlannedWorkout.objects.create(
|
||||
workout=gw.workout,
|
||||
registered_user=registered_user,
|
||||
on_date=gw.scheduled_date,
|
||||
)
|
||||
|
||||
if workouts_to_update:
|
||||
GeneratedWorkout.objects.bulk_update(workouts_to_update, ['status'])
|
||||
|
||||
serializer = GeneratedWeeklyPlanSerializer(plan)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -815,10 +843,10 @@ def confirm_plan(request, plan_id):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def preview_plan(request):
|
||||
"""
|
||||
Generate a weekly plan preview. Returns JSON — nothing is saved to DB.
|
||||
Generate a weekly plan preview. Returns JSON -- nothing is saved to DB.
|
||||
Body: {"week_start_date": "YYYY-MM-DD"}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
|
||||
week_start_date_str = request.data.get('week_start_date')
|
||||
if not week_start_date_str:
|
||||
@@ -872,8 +900,9 @@ def preview_plan(request):
|
||||
)
|
||||
preview = generator.generate_weekly_preview(week_start_date)
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in preview_plan")
|
||||
return Response(
|
||||
{'error': f'Preview generation failed: {str(e)}'},
|
||||
{"error": "An unexpected error occurred. Please try again."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -885,7 +914,7 @@ def preview_plan(request):
|
||||
@permission_classes([IsAuthenticated])
|
||||
def preview_day(request):
|
||||
"""
|
||||
Generate a single day preview. Returns JSON — nothing is saved to DB.
|
||||
Generate a single day preview. Returns JSON -- nothing is saved to DB.
|
||||
Body: {
|
||||
"target_muscles": ["chest", "shoulders"],
|
||||
"focus_area": "Upper Push",
|
||||
@@ -893,7 +922,7 @@ def preview_day(request):
|
||||
"date": "2026-02-09"
|
||||
}
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
|
||||
date_str = request.data.get('date')
|
||||
if not date_str:
|
||||
@@ -954,26 +983,19 @@ def preview_day(request):
|
||||
|
||||
generator = WorkoutGenerator(preference)
|
||||
|
||||
# If plan_id is provided, exclude sibling workout exercises
|
||||
# If plan_id is provided, exclude sibling workout exercises (single query)
|
||||
if plan_id is not None:
|
||||
try:
|
||||
plan = GeneratedWeeklyPlan.objects.get(
|
||||
pk=plan_id,
|
||||
registered_user=registered_user,
|
||||
)
|
||||
sibling_workouts = GeneratedWorkout.objects.filter(
|
||||
plan=plan,
|
||||
is_rest_day=False,
|
||||
workout__isnull=False,
|
||||
sibling_exercise_ids = set(
|
||||
SupersetExercise.objects.filter(
|
||||
superset__workout__generated_from__plan=plan,
|
||||
superset__workout__generated_from__is_rest_day=False,
|
||||
).values_list('exercise_id', flat=True)
|
||||
)
|
||||
sibling_exercise_ids = set()
|
||||
for sibling in sibling_workouts:
|
||||
if sibling.workout:
|
||||
sibling_exercise_ids.update(
|
||||
SupersetExercise.objects.filter(
|
||||
superset__workout=sibling.workout
|
||||
).values_list('exercise_id', flat=True)
|
||||
)
|
||||
if sibling_exercise_ids:
|
||||
generator.exercise_selector.hard_exclude_ids.update(sibling_exercise_ids)
|
||||
except GeneratedWeeklyPlan.DoesNotExist:
|
||||
@@ -987,8 +1009,9 @@ def preview_day(request):
|
||||
if plan_id is not None:
|
||||
day_preview['plan_id'] = plan_id
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in preview_day")
|
||||
return Response(
|
||||
{'error': f'Day preview generation failed: {str(e)}'},
|
||||
{"error": "An unexpected error occurred. Please try again."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
@@ -1003,7 +1026,7 @@ def save_plan(request):
|
||||
Save a preview plan to the database.
|
||||
Body: the full preview JSON (same shape as preview_plan response).
|
||||
"""
|
||||
registered_user = RegisteredUser.objects.get(user=request.user)
|
||||
registered_user = get_registered_user(request)
|
||||
|
||||
week_start_date_str = request.data.get('week_start_date')
|
||||
days = request.data.get('days', [])
|
||||
@@ -1057,105 +1080,130 @@ def save_plan(request):
|
||||
),
|
||||
}
|
||||
|
||||
plan = GeneratedWeeklyPlan.objects.create(
|
||||
registered_user=registered_user,
|
||||
week_start_date=week_start_date,
|
||||
week_end_date=week_end_date,
|
||||
status='completed',
|
||||
preferences_snapshot=prefs_snapshot,
|
||||
)
|
||||
|
||||
# Prefetch all exercise IDs referenced in the plan to avoid N+1 queries
|
||||
all_exercise_ids = []
|
||||
for day_data in days:
|
||||
day_date_str = day_data.get('date')
|
||||
scheduled_date = datetime.strptime(day_date_str, '%Y-%m-%d').date()
|
||||
day_of_week = scheduled_date.weekday()
|
||||
is_rest_day = day_data.get('is_rest_day', False)
|
||||
|
||||
if is_rest_day:
|
||||
GeneratedWorkout.objects.create(
|
||||
plan=plan,
|
||||
workout=None,
|
||||
workout_type=None,
|
||||
scheduled_date=scheduled_date,
|
||||
day_of_week=day_of_week,
|
||||
is_rest_day=True,
|
||||
status='accepted',
|
||||
focus_area='Rest Day',
|
||||
target_muscles=[],
|
||||
)
|
||||
if day_data.get('is_rest_day', False):
|
||||
continue
|
||||
|
||||
workout_spec_data = day_data.get('workout_spec', {})
|
||||
focus_area = day_data.get('focus_area', 'Workout')
|
||||
target_muscles = day_data.get('target_muscles', [])
|
||||
workout_type_id = day_data.get('workout_type_id')
|
||||
|
||||
workout_type = None
|
||||
if workout_type_id:
|
||||
workout_type = WorkoutType.objects.filter(pk=workout_type_id).first()
|
||||
|
||||
supersets_data = workout_spec_data.get('supersets', [])
|
||||
orm_supersets = []
|
||||
for ss_data in supersets_data:
|
||||
exercises = []
|
||||
for ss_data in workout_spec_data.get('supersets', []):
|
||||
for ex_data in ss_data.get('exercises', []):
|
||||
exercise_id = ex_data.get('exercise_id')
|
||||
if not exercise_id:
|
||||
continue
|
||||
try:
|
||||
exercise_obj = Exercise.objects.get(pk=exercise_id)
|
||||
except Exercise.DoesNotExist:
|
||||
continue
|
||||
if exercise_id:
|
||||
all_exercise_ids.append(exercise_id)
|
||||
|
||||
exercises.append({
|
||||
'exercise': exercise_obj,
|
||||
'reps': ex_data.get('reps'),
|
||||
'duration': ex_data.get('duration'),
|
||||
'weight': ex_data.get('weight'),
|
||||
'order': ex_data.get('order', 1),
|
||||
exercises_map = {
|
||||
e.id: e for e in Exercise.objects.filter(id__in=all_exercise_ids)
|
||||
}
|
||||
|
||||
# Prefetch all workout type IDs referenced in the plan
|
||||
all_workout_type_ids = []
|
||||
for day_data in days:
|
||||
wt_id = day_data.get('workout_type_id')
|
||||
if wt_id:
|
||||
all_workout_type_ids.append(wt_id)
|
||||
workout_types_map = {
|
||||
wt.id: wt for wt in WorkoutType.objects.filter(id__in=all_workout_type_ids)
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
plan = GeneratedWeeklyPlan.objects.create(
|
||||
registered_user=registered_user,
|
||||
week_start_date=week_start_date,
|
||||
week_end_date=week_end_date,
|
||||
status='completed',
|
||||
preferences_snapshot=prefs_snapshot,
|
||||
)
|
||||
|
||||
for day_data in days:
|
||||
day_date_str = day_data.get('date')
|
||||
scheduled_date = datetime.strptime(day_date_str, '%Y-%m-%d').date()
|
||||
day_of_week = scheduled_date.weekday()
|
||||
is_rest_day = day_data.get('is_rest_day', False)
|
||||
|
||||
if is_rest_day:
|
||||
GeneratedWorkout.objects.create(
|
||||
plan=plan,
|
||||
workout=None,
|
||||
workout_type=None,
|
||||
scheduled_date=scheduled_date,
|
||||
day_of_week=day_of_week,
|
||||
is_rest_day=True,
|
||||
status='accepted',
|
||||
focus_area='Rest Day',
|
||||
target_muscles=[],
|
||||
)
|
||||
continue
|
||||
|
||||
workout_spec_data = day_data.get('workout_spec', {})
|
||||
focus_area = day_data.get('focus_area', 'Workout')
|
||||
target_muscles = day_data.get('target_muscles', [])
|
||||
workout_type_id = day_data.get('workout_type_id')
|
||||
|
||||
workout_type = workout_types_map.get(workout_type_id) if workout_type_id else None
|
||||
|
||||
supersets_data = workout_spec_data.get('supersets', [])
|
||||
orm_supersets = []
|
||||
for ss_data in supersets_data:
|
||||
exercises = []
|
||||
for ex_data in ss_data.get('exercises', []):
|
||||
exercise_id = ex_data.get('exercise_id')
|
||||
if not exercise_id:
|
||||
continue
|
||||
exercise_obj = exercises_map.get(exercise_id)
|
||||
if not exercise_obj:
|
||||
continue
|
||||
|
||||
exercises.append({
|
||||
'exercise': exercise_obj,
|
||||
'reps': ex_data.get('reps'),
|
||||
'duration': ex_data.get('duration'),
|
||||
'weight': ex_data.get('weight'),
|
||||
'order': ex_data.get('order', 1),
|
||||
})
|
||||
|
||||
orm_supersets.append({
|
||||
'name': ss_data.get('name', 'Set'),
|
||||
'rounds': ss_data.get('rounds', 1),
|
||||
'rest_between_rounds': ss_data.get('rest_between_rounds', 0),
|
||||
'exercises': exercises,
|
||||
})
|
||||
|
||||
orm_supersets.append({
|
||||
'name': ss_data.get('name', 'Set'),
|
||||
'rounds': ss_data.get('rounds', 1),
|
||||
'rest_between_rounds': ss_data.get('rest_between_rounds', 0),
|
||||
'exercises': exercises,
|
||||
})
|
||||
workout_spec = {
|
||||
'name': workout_spec_data.get('name', f'{focus_area} Workout'),
|
||||
'description': workout_spec_data.get('description', ''),
|
||||
'supersets': orm_supersets,
|
||||
}
|
||||
|
||||
workout_spec = {
|
||||
'name': workout_spec_data.get('name', f'{focus_area} Workout'),
|
||||
'description': workout_spec_data.get('description', ''),
|
||||
'supersets': orm_supersets,
|
||||
}
|
||||
workout_obj = plan_builder.create_workout_from_spec(workout_spec)
|
||||
|
||||
workout_obj = plan_builder.create_workout_from_spec(workout_spec)
|
||||
GeneratedWorkout.objects.create(
|
||||
plan=plan,
|
||||
workout=workout_obj,
|
||||
workout_type=workout_type,
|
||||
scheduled_date=scheduled_date,
|
||||
day_of_week=day_of_week,
|
||||
is_rest_day=False,
|
||||
status='accepted',
|
||||
focus_area=focus_area,
|
||||
target_muscles=target_muscles,
|
||||
)
|
||||
|
||||
GeneratedWorkout.objects.create(
|
||||
plan=plan,
|
||||
workout=workout_obj,
|
||||
workout_type=workout_type,
|
||||
scheduled_date=scheduled_date,
|
||||
day_of_week=day_of_week,
|
||||
is_rest_day=False,
|
||||
status='accepted',
|
||||
focus_area=focus_area,
|
||||
target_muscles=target_muscles,
|
||||
)
|
||||
|
||||
# Create/replace PlannedWorkout for this date
|
||||
PlannedWorkout.objects.filter(
|
||||
registered_user=registered_user,
|
||||
on_date=scheduled_date,
|
||||
).delete()
|
||||
PlannedWorkout.objects.create(
|
||||
workout=workout_obj,
|
||||
registered_user=registered_user,
|
||||
on_date=scheduled_date,
|
||||
)
|
||||
# Create/replace PlannedWorkout for this date
|
||||
PlannedWorkout.objects.filter(
|
||||
registered_user=registered_user,
|
||||
on_date=scheduled_date,
|
||||
).delete()
|
||||
PlannedWorkout.objects.create(
|
||||
workout=workout_obj,
|
||||
registered_user=registered_user,
|
||||
on_date=scheduled_date,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error in save_plan")
|
||||
return Response(
|
||||
{'error': f'Save failed: {str(e)}'},
|
||||
{"error": "An unexpected error occurred. Please try again."},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user