Hardening follow-up: N+1 elimination, type validation, diversify fix
Additional fixes from parallel hardening streams: - exercise/serializers: remove unused WorkoutEquipment import, add prefetch docs - generator/serializers: N+1 fix in GeneratedWorkoutDetailSerializer (inline workout dict, prefetch-aware supersets) - generator/services/plan_builder: eliminate redundant .save() after .create() via single create_kwargs dict - generator/services/workout_generator: proper type-match validation for HIIT/cardio/core/flexibility; fix diversify type count to account for removed entry - generator/views: request-level caching for get_registered_user helper; prefetch chain for accept_workout - superset/serializers: guard against dangling FK in SupersetExerciseSerializer - workout/helpers: use prefetched data instead of re-querying per superset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,26 +96,27 @@ class PlanBuilder:
|
||||
|
||||
order = ex_spec.get('order', 1)
|
||||
|
||||
superset_exercise = SupersetExercise.objects.create(
|
||||
superset=superset,
|
||||
exercise=exercise_obj,
|
||||
order=order,
|
||||
)
|
||||
# Build kwargs for create, including optional fields,
|
||||
# so we don't need a separate .save() after .create().
|
||||
create_kwargs = {
|
||||
'superset': superset,
|
||||
'exercise': exercise_obj,
|
||||
'order': order,
|
||||
}
|
||||
|
||||
# Assign optional fields exactly like add_workout does
|
||||
if ex_spec.get('weight') is not None:
|
||||
superset_exercise.weight = ex_spec['weight']
|
||||
create_kwargs['weight'] = ex_spec['weight']
|
||||
|
||||
if ex_spec.get('reps') is not None:
|
||||
superset_exercise.reps = ex_spec['reps']
|
||||
create_kwargs['reps'] = ex_spec['reps']
|
||||
rep_duration = exercise_obj.estimated_rep_duration or 3.0
|
||||
superset_total_time += ex_spec['reps'] * rep_duration
|
||||
|
||||
if ex_spec.get('duration') is not None:
|
||||
superset_exercise.duration = ex_spec['duration']
|
||||
create_kwargs['duration'] = ex_spec['duration']
|
||||
superset_total_time += ex_spec['duration']
|
||||
|
||||
superset_exercise.save()
|
||||
SupersetExercise.objects.create(**create_kwargs)
|
||||
|
||||
# ---- 4. Update superset estimated_time ----
|
||||
# Store total time including all rounds and rest between rounds
|
||||
|
||||
@@ -1398,18 +1398,31 @@ class WorkoutGenerator:
|
||||
break
|
||||
|
||||
replaced = False
|
||||
removed_type = (result[idx].get('split_type') or 'full_body').strip().lower()
|
||||
removed_sig = self._split_signature(result[idx])
|
||||
for candidate in candidates:
|
||||
candidate_type = (candidate.get('split_type') or 'full_body').strip().lower()
|
||||
candidate_sig = self._split_signature(candidate)
|
||||
current_sig = self._split_signature(result[idx])
|
||||
if candidate_sig == current_sig:
|
||||
if candidate_sig == removed_sig:
|
||||
continue
|
||||
|
||||
new_type_count = type_counts[candidate_type] + (0 if candidate_type == (result[idx].get('split_type') or 'full_body').strip().lower() else 1)
|
||||
# Account for the removal of the old entry when counting
|
||||
# the new type: subtract 1 for the removed type if it
|
||||
# matches the candidate type, add 1 for the candidate.
|
||||
if candidate_type == removed_type:
|
||||
new_type_count = type_counts[candidate_type] # net zero: -1 removed +1 added
|
||||
else:
|
||||
new_type_count = type_counts[candidate_type] + 1
|
||||
if new_type_count > max_same_type:
|
||||
continue
|
||||
|
||||
if sig_counts[candidate_sig] >= max_same_signature:
|
||||
# Same accounting for signatures: the removed signature
|
||||
# frees a slot, so only block if the candidate sig count
|
||||
# (after removing the old entry) is still at max.
|
||||
effective_sig_count = sig_counts[candidate_sig]
|
||||
if candidate_sig == removed_sig:
|
||||
effective_sig_count -= 1
|
||||
if effective_sig_count >= max_same_signature:
|
||||
continue
|
||||
|
||||
result[idx] = dict(candidate)
|
||||
@@ -2987,7 +3000,12 @@ class WorkoutGenerator:
|
||||
return []
|
||||
|
||||
wt_name_lower = workout_type.name.strip().lower()
|
||||
wt_key = _normalize_type_key(wt_name_lower)
|
||||
is_strength = wt_name_lower in STRENGTH_WORKOUT_TYPES
|
||||
is_hiit = wt_key == 'high_intensity_interval_training'
|
||||
is_cardio = wt_key == 'cardio'
|
||||
is_core = wt_key == 'core_training'
|
||||
is_flexibility = wt_key == 'flexibility'
|
||||
threshold = GENERATION_RULES['workout_type_match_pct']['value']
|
||||
|
||||
total_exercises = 0
|
||||
@@ -3001,7 +3019,33 @@ class WorkoutGenerator:
|
||||
if is_strength:
|
||||
if getattr(ex, 'is_weight', False) or getattr(ex, 'is_compound', False):
|
||||
matching_exercises += 1
|
||||
elif is_hiit:
|
||||
# HIIT: favor high HR, compound, or duration-capable exercises
|
||||
hr = getattr(ex, 'hr_elevation_rating', None) or 0
|
||||
if hr >= 5 or getattr(ex, 'is_compound', False) or getattr(ex, 'is_duration', False):
|
||||
matching_exercises += 1
|
||||
elif is_cardio:
|
||||
# Cardio: favor duration-capable or high-HR exercises
|
||||
hr = getattr(ex, 'hr_elevation_rating', None) or 0
|
||||
if getattr(ex, 'is_duration', False) or hr >= 5:
|
||||
matching_exercises += 1
|
||||
elif is_core:
|
||||
# Core: check if exercise targets core muscles
|
||||
muscles = (getattr(ex, 'muscle_groups', '') or '').lower()
|
||||
patterns = (getattr(ex, 'movement_patterns', '') or '').lower()
|
||||
if any(tok in muscles for tok in ('core', 'abs', 'oblique')):
|
||||
matching_exercises += 1
|
||||
elif 'core' in patterns or 'anti' in patterns:
|
||||
matching_exercises += 1
|
||||
elif is_flexibility:
|
||||
# Flexibility: favor duration-based, stretch/mobility exercises
|
||||
patterns = (getattr(ex, 'movement_patterns', '') or '').lower()
|
||||
if getattr(ex, 'is_duration', False) or any(
|
||||
tok in patterns for tok in ('stretch', 'mobility', 'yoga', 'flexibility')
|
||||
):
|
||||
matching_exercises += 1
|
||||
else:
|
||||
# Unknown type -- count all as matching (no false negatives)
|
||||
matching_exercises += 1
|
||||
|
||||
violations = []
|
||||
|
||||
Reference in New Issue
Block a user