from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from registered_user.models import RegisteredUser from workout.models import Workout from exercise.models import Exercise from equipment.models import Equipment from muscle.models import Muscle INTENSITY_CHOICES = ( ('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ) GOAL_CHOICES = ( ('strength', 'Strength'), ('hypertrophy', 'Hypertrophy'), ('endurance', 'Endurance'), ('weight_loss', 'Weight Loss'), ('general_fitness', 'General Fitness'), ) FITNESS_LEVEL_CHOICES = ( (1, 'Beginner'), (2, 'Intermediate'), (3, 'Advanced'), (4, 'Elite'), ) PLAN_STATUS_CHOICES = ( ('pending', 'Pending'), ('completed', 'Completed'), ('failed', 'Failed'), ) WORKOUT_STATUS_CHOICES = ( ('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected'), ('completed', 'Completed'), ) SPLIT_TYPE_CHOICES = ( ('push', 'Push'), ('pull', 'Pull'), ('legs', 'Legs'), ('upper', 'Upper'), ('lower', 'Lower'), ('full_body', 'Full Body'), ('core', 'Core'), ('cardio', 'Cardio'), ) SECTION_TYPE_CHOICES = ( ('warm_up', 'Warm Up'), ('working', 'Working'), ('cool_down', 'Cool Down'), ) POSITION_CHOICES = ( ('early', 'Early'), ('middle', 'Middle'), ('late', 'Late'), ) # ============================================================ # Reference / Config Models # ============================================================ class WorkoutType(models.Model): name = models.CharField(max_length=100, unique=True) display_name = models.CharField(max_length=100, blank=True, default='') description = models.TextField(blank=True, default='') typical_rest_between_sets = models.IntegerField(default=60, help_text='Seconds') typical_intensity = models.CharField(max_length=10, choices=INTENSITY_CHOICES, default='medium') rep_range_min = models.IntegerField(default=8) rep_range_max = models.IntegerField(default=12) round_range_min = models.IntegerField(default=3) round_range_max = models.IntegerField(default=4) duration_bias = models.FloatField(default=0.5, help_text='0.0=all rep-based, 1.0=all duration-based') superset_size_min = models.IntegerField(default=2) superset_size_max = models.IntegerField(default=4) def __str__(self): return self.name # ============================================================ # User Preference Model # ============================================================ class UserPreference(models.Model): registered_user = models.OneToOneField( RegisteredUser, on_delete=models.CASCADE, related_name='generator_preference' ) available_equipment = models.ManyToManyField(Equipment, blank=True, related_name='user_preferences') target_muscle_groups = models.ManyToManyField(Muscle, blank=True, related_name='user_preferences') preferred_workout_types = models.ManyToManyField(WorkoutType, blank=True, related_name='user_preferences') fitness_level = models.IntegerField(choices=FITNESS_LEVEL_CHOICES, default=2) primary_goal = models.CharField(max_length=20, choices=GOAL_CHOICES, default='general_fitness') secondary_goal = models.CharField(max_length=20, choices=GOAL_CHOICES, blank=True, default='') days_per_week = models.IntegerField(default=4) preferred_workout_duration = models.IntegerField( default=45, help_text='Minutes', validators=[MinValueValidator(15), MaxValueValidator(120)], ) preferred_days = models.JSONField(default=list, blank=True, help_text='List of weekday ints (0=Mon, 6=Sun)') injuries_limitations = models.TextField(blank=True, default='') injury_types = models.JSONField( default=list, blank=True, help_text='Structured injury types: knee, lower_back, upper_back, shoulder, hip, wrist, ankle, neck', ) excluded_exercises = models.ManyToManyField(Exercise, blank=True, related_name='excluded_by_users') def __str__(self): return f"Preferences for {self.registered_user.first_name}" # ============================================================ # Generated Plan / Workout Models # ============================================================ class GeneratedWeeklyPlan(models.Model): created_at = models.DateTimeField(auto_now_add=True) registered_user = models.ForeignKey( RegisteredUser, on_delete=models.CASCADE, related_name='generated_plans' ) week_start_date = models.DateField() week_end_date = models.DateField() status = models.CharField(max_length=10, choices=PLAN_STATUS_CHOICES, default='pending') preferences_snapshot = models.JSONField(default=dict, blank=True) generation_time_ms = models.IntegerField(null=True, blank=True) # Periodization fields week_number = models.IntegerField(default=1, help_text='Position in training cycle (1-based)') is_deload = models.BooleanField(default=False, help_text='Whether this is a recovery/deload week') cycle_id = models.CharField(max_length=64, null=True, blank=True, help_text='Groups weeks into training cycles') class Meta: ordering = ['-created_at'] def __str__(self): return f"Plan {self.id} for {self.registered_user.first_name} ({self.week_start_date})" class GeneratedWorkout(models.Model): plan = models.ForeignKey( GeneratedWeeklyPlan, on_delete=models.CASCADE, related_name='generated_workouts' ) workout = models.OneToOneField( Workout, on_delete=models.SET_NULL, null=True, blank=True, related_name='generated_from' ) workout_type = models.ForeignKey( WorkoutType, on_delete=models.SET_NULL, null=True, blank=True, related_name='generated_workouts' ) scheduled_date = models.DateField() day_of_week = models.IntegerField(help_text='0=Monday, 6=Sunday') is_rest_day = models.BooleanField(default=False) status = models.CharField(max_length=10, choices=WORKOUT_STATUS_CHOICES, default='pending') focus_area = models.CharField(max_length=255, blank=True, default='') target_muscles = models.JSONField(default=list, blank=True) user_rating = models.IntegerField(null=True, blank=True) user_feedback = models.TextField(blank=True, default='') class Meta: ordering = ['scheduled_date'] def __str__(self): if self.is_rest_day: return f"Rest Day - {self.scheduled_date}" return f"{self.focus_area} - {self.scheduled_date}" # ============================================================ # ML Pattern Models (populated by analyze_workouts command) # ============================================================ class MuscleGroupSplit(models.Model): muscle_names = models.JSONField(default=list, help_text='List of muscle group names') frequency = models.IntegerField(default=0, help_text='How often this combo appeared') label = models.CharField(max_length=100, blank=True, default='') typical_exercise_count = models.IntegerField(default=6) split_type = models.CharField(max_length=20, choices=SPLIT_TYPE_CHOICES, default='full_body') def __str__(self): return f"{self.label} ({self.split_type}) - freq: {self.frequency}" class WeeklySplitPattern(models.Model): days_per_week = models.IntegerField() pattern = models.JSONField(default=list, help_text='Ordered list of MuscleGroupSplit IDs') pattern_labels = models.JSONField(default=list, help_text='Ordered list of split labels') frequency = models.IntegerField(default=0) rest_day_positions = models.JSONField(default=list, help_text='Day indices that are rest days') def __str__(self): return f"{self.days_per_week}-day split (freq: {self.frequency}): {self.pattern_labels}" class WorkoutStructureRule(models.Model): workout_type = models.ForeignKey( WorkoutType, on_delete=models.CASCADE, null=True, blank=True, related_name='structure_rules' ) section_type = models.CharField(max_length=10, choices=SECTION_TYPE_CHOICES) movement_patterns = models.JSONField(default=list) typical_rounds = models.IntegerField(default=3) typical_exercises_per_superset = models.IntegerField(default=3) typical_rep_range_min = models.IntegerField(default=8) typical_rep_range_max = models.IntegerField(default=12) typical_duration_range_min = models.IntegerField(default=30, help_text='Seconds') typical_duration_range_max = models.IntegerField(default=45, help_text='Seconds') goal_type = models.CharField(max_length=20, choices=GOAL_CHOICES, default='general_fitness') def __str__(self): wt = self.workout_type.name if self.workout_type else 'Any' return f"{wt} - {self.section_type} ({self.goal_type})" class MovementPatternOrder(models.Model): position = models.CharField(max_length=10, choices=POSITION_CHOICES) movement_pattern = models.CharField(max_length=100) frequency = models.IntegerField(default=0) section_type = models.CharField(max_length=10, choices=SECTION_TYPE_CHOICES, default='working') class Meta: ordering = ['position', '-frequency'] def __str__(self): return f"{self.movement_pattern} @ {self.position} (freq: {self.frequency})"