""" Fix exercise flags and assign missing muscle associations. 1. Fix is_weight flags on exercises that are bodyweight but incorrectly marked is_weight=True (wall sits, agility ladder, planks, bodyweight exercises, etc.) 2. Assign muscle groups to exercises that have no ExerciseMuscle rows, using name keyword matching. Known false positives: wall sits, agility ladder, planks, body weight exercises, and similar movements that use no external resistance. Usage: python manage.py fix_exercise_flags python manage.py fix_exercise_flags --dry-run """ import re from django.core.management.base import BaseCommand from exercise.models import Exercise from muscle.models import Muscle, ExerciseMuscle try: from equipment.models import WorkoutEquipment except ImportError: WorkoutEquipment = None # Patterns that indicate bodyweight exercises (no external weight). # Uses word boundary matching to avoid substring issues (e.g. "l sit" in "wall sit"). BODYWEIGHT_PATTERNS = [ r'\bwall sit\b', r'\bplank\b', r'\bmountain climber\b', r'\bburpee\b', r'\bpush ?up\b', r'\bpushup\b', r'\bpull ?up\b', r'\bpullup\b', r'\bchin ?up\b', r'\bchinup\b', r'\bdips?\b', r'\bpike\b', r'\bhandstand\b', r'\bl sit\b', r'\bv sit\b', r'\bhollow\b', r'\bsuperman\b', r'\bbird dog\b', r'\bdead bug\b', r'\bbear crawl\b', r'\bcrab walk\b', r'\binchworm\b', r'\bjumping jack\b', r'\bhigh knee\b', r'\bbutt kick\b', r'\bskater\b', r'\blunge jump\b', r'\bjump lunge\b', r'\bsquat jump\b', r'\bjump squat\b', r'\bbox jump\b', r'\btuck jump\b', r'\bbroad jump\b', r'\bsprinter\b', r'\bagility ladder\b', r'\bbody ?weight\b', r'\bbodyweight\b', r'\bcalisthenics?\b', r'\bflutter kick\b', r'\bleg raise\b', r'\bsit ?up\b', r'\bcrunch\b', r'\bstretch\b', r'\byoga\b', r'\bfoam roll\b', r'\bjump rope\b', r'\bspider crawl\b', ] # Keywords for assigning muscles to exercises with no ExerciseMuscle rows. # Each muscle name maps to a list of name keywords to match against exercise name. EXERCISE_MUSCLE_KEYWORDS = { 'chest': ['chest', 'pec', 'bench press', 'push up', 'fly'], 'back': ['back', 'lat', 'row', 'pull up', 'pulldown'], 'shoulders': ['shoulder', 'delt', 'press', 'raise', 'shrug'], 'quads': ['quad', 'squat', 'leg press', 'lunge', 'extension'], 'hamstrings': ['hamstring', 'curl', 'deadlift', 'rdl'], 'glutes': ['glute', 'hip thrust', 'bridge'], 'biceps': ['bicep', 'curl'], 'triceps': ['tricep', 'pushdown', 'extension', 'dip'], 'core': ['core', 'ab', 'crunch', 'plank', 'sit up'], 'calves': ['calf', 'calves', 'calf raise'], } class Command(BaseCommand): help = 'Fix is_weight flags and assign missing muscle associations' def add_arguments(self, parser): parser.add_argument( '--dry-run', action='store_true', help='Show what would change without writing to DB', ) def handle(self, *args, **options): dry_run = options['dry_run'] self.stdout.write(self.style.MIGRATE_HEADING('Step 1: Fix is_weight false positives')) weight_fixed = self._fix_is_weight_false_positives(dry_run) self.stdout.write(self.style.MIGRATE_HEADING('\nStep 2: Assign missing muscles')) muscle_assigned = self._assign_missing_muscles(dry_run) prefix = '[DRY RUN] ' if dry_run else '' self.stdout.write(self.style.SUCCESS( f'\n{prefix}Summary: Fixed {weight_fixed} is_weight flags, ' f'assigned muscles to {muscle_assigned} exercises' )) def _fix_is_weight_false_positives(self, dry_run): """Fix exercises that are bodyweight but incorrectly marked is_weight=True.""" # Get exercises that have is_weight=True weighted_exercises = Exercise.objects.filter(is_weight=True) # Get exercises that have equipment assigned (if WorkoutEquipment exists) exercises_with_equipment = set() if WorkoutEquipment is not None: exercises_with_equipment = set( WorkoutEquipment.objects.values_list('exercise_id', flat=True).distinct() ) fixed = 0 for ex in weighted_exercises: if not ex.name: continue name_lower = ex.name.lower() # Check if name matches any bodyweight pattern is_bodyweight_name = any( re.search(pat, name_lower) for pat in BODYWEIGHT_PATTERNS ) # Also check if the exercise has no equipment assigned has_no_equipment = ex.pk not in exercises_with_equipment if is_bodyweight_name and has_no_equipment: if dry_run: self.stdout.write(f' Would fix: {ex.name} (id={ex.pk})') else: ex.is_weight = False ex.save(update_fields=['is_weight']) self.stdout.write(f' Fixed: {ex.name} (id={ex.pk})') fixed += 1 prefix = '[DRY RUN] ' if dry_run else '' self.stdout.write(self.style.SUCCESS( f'{prefix}Fixed {fixed} exercises from is_weight=True to is_weight=False' )) return fixed def _assign_missing_muscles(self, dry_run): """Assign muscle groups to exercises that have no ExerciseMuscle rows.""" # Find exercises with no muscle associations exercises_with_muscles = set( ExerciseMuscle.objects.values_list('exercise_id', flat=True).distinct() ) orphan_exercises = Exercise.objects.exclude(pk__in=exercises_with_muscles) if not orphan_exercises.exists(): self.stdout.write(' No exercises without muscle assignments found.') return 0 self.stdout.write(f' Found {orphan_exercises.count()} exercises without muscle assignments') # Build a cache of muscle objects by name (case-insensitive) muscle_cache = {} for muscle in Muscle.objects.all(): muscle_cache[muscle.name.lower()] = muscle assigned_count = 0 for ex in orphan_exercises: if not ex.name: continue name_lower = ex.name.lower() matched_muscles = [] for muscle_name, keywords in EXERCISE_MUSCLE_KEYWORDS.items(): for keyword in keywords: if keyword in name_lower: # Find the muscle in the cache muscle_obj = muscle_cache.get(muscle_name) if muscle_obj and muscle_obj not in matched_muscles: matched_muscles.append(muscle_obj) break # One keyword match per muscle group is enough if matched_muscles: if dry_run: muscle_names = ', '.join(m.name for m in matched_muscles) self.stdout.write( f' Would assign: {ex.name} (id={ex.pk}) -> [{muscle_names}]' ) else: for muscle_obj in matched_muscles: ExerciseMuscle.objects.get_or_create( exercise=ex, muscle=muscle_obj, ) muscle_names = ', '.join(m.name for m in matched_muscles) self.stdout.write( f' Assigned: {ex.name} (id={ex.pk}) -> [{muscle_names}]' ) assigned_count += 1 prefix = '[DRY RUN] ' if dry_run else '' self.stdout.write(self.style.SUCCESS( f'{prefix}Assigned muscles to {assigned_count} exercises' )) return assigned_count