""" Normalize muscle names in the database and merge duplicates. Uses the MUSCLE_NORMALIZATION_MAP from muscle_normalizer.py to: 1. Rename each Muscle record to its canonical lowercase form 2. Merge duplicates by updating ExerciseMuscle FKs to point to the canonical Muscle 3. Delete orphaned duplicate Muscle records Usage: python manage.py normalize_muscle_names --dry-run python manage.py normalize_muscle_names """ from collections import defaultdict from django.core.management.base import BaseCommand from django.db import transaction from muscle.models import Muscle, ExerciseMuscle from generator.services.muscle_normalizer import normalize_muscle_name class Command(BaseCommand): help = 'Normalize muscle names and merge duplicates using MUSCLE_NORMALIZATION_MAP' 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'] all_muscles = Muscle.objects.all().order_by('id') self.stdout.write(f'Found {all_muscles.count()} muscle records') # Group muscles by their canonical name canonical_groups = defaultdict(list) for muscle in all_muscles: canonical = normalize_muscle_name(muscle.name) if canonical: canonical_groups[canonical].append(muscle) renamed = 0 merged = 0 deleted = 0 with transaction.atomic(): for canonical_name, muscles in canonical_groups.items(): # Pick the keeper: prefer the one with the lowest ID (oldest) keeper = muscles[0] # Rename keeper if needed if keeper.name != canonical_name: if dry_run: self.stdout.write(f' Rename: "{keeper.name}" -> "{canonical_name}" (id={keeper.pk})') else: keeper.name = canonical_name keeper.save(update_fields=['name']) renamed += 1 # Merge duplicates into keeper for dup in muscles[1:]: # Count affected ExerciseMuscle rows em_count = ExerciseMuscle.objects.filter(muscle=dup).count() if dry_run: self.stdout.write( f' Merge: "{dup.name}" (id={dup.pk}) -> "{canonical_name}" ' f'(id={keeper.pk}), {em_count} ExerciseMuscle rows' ) else: # Update ExerciseMuscle FKs, handling unique_together conflicts for em in ExerciseMuscle.objects.filter(muscle=dup): # Check if keeper already has this exercise existing = ExerciseMuscle.objects.filter( exercise=em.exercise, muscle=keeper ).exists() if existing: em.delete() else: em.muscle = keeper em.save(update_fields=['muscle']) dup.delete() merged += em_count deleted += 1 if dry_run: # Roll back the transaction for dry run transaction.set_rollback(True) prefix = '[DRY RUN] ' if dry_run else '' self.stdout.write(self.style.SUCCESS( f'\n{prefix}Results:' f'\n Renamed: {renamed} muscles' f'\n Merged: {merged} ExerciseMuscle references' f'\n Deleted: {deleted} duplicate Muscle records' )) # Verify if not dry_run: dupes = ( Muscle.objects.values('name') .annotate(c=__import__('django.db.models', fromlist=['Count']).Count('id')) .filter(c__gt=1) ) if dupes.exists(): self.stdout.write(self.style.WARNING( f' WARNING: {dupes.count()} duplicate names still exist!' )) else: self.stdout.write(self.style.SUCCESS(' No duplicate muscle names remain.'))