- Add rules_engine.py with quantitative rules for all 8 workout types - Add quality gate retry loop in generate_single_workout() - Expand calibrate_structure_rules to all 120 combinations (8 types × 5 goals × 3 sections) - Wire WeeklySplitPattern DB records into _pick_weekly_split() - Enforce movement patterns from WorkoutStructureRule in exercise selection - Add straight-set strength support (single main lift, 4-6 rounds) - Add modality consistency check for duration-dominant workout types - Add InjuryStep component to onboarding and preferences - Add sibling exercise exclusion in regenerate and preview_day endpoints - Display generator warnings on dashboard - Expand fix_rep_durations, fix_exercise_flags, fix_movement_pattern_typo - Add audit_exercise_data and check_rules_drift management commands - Add Next.js frontend with dashboard, onboarding, preferences, history pages - Add generator app with ML-powered workout generation pipeline - 96 new tests across 7 test modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
"""
|
|
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.'))
|