from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils import timezone class UserProgress(models.Model): STATUS_CHOICES = [ ('NOT_STARTED', 'Not Started'), ('IN_PROGRESS', 'In Progress'), ('COMPLETED', 'Completed'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='resource_progress', ) resource = models.ForeignKey( 'resources.Resource', on_delete=models.CASCADE, related_name='user_progress', ) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='NOT_STARTED') progress = models.IntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='0-100. Derived from checkpoint rollup when checkpoints exist, else manual slider.', ) started_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True) class Meta: unique_together = [['user', 'resource']] indexes = [models.Index(fields=['user', 'status'])] def __str__(self): return f'{self.user.email} - {self.resource.title} ({self.status})' def recalculate_from_checkpoints(self, save=True): """Recompute status + progress from UserCheckpointProgress rollup. Returns True when the resource has checkpoints (rollup applied); False when the resource is manual-slider-only (caller owns status/progress). """ checkpoints = list(self.resource.checkpoints.all()) if not checkpoints: return False total = len(checkpoints) done = UserCheckpointProgress.objects.filter( user=self.user, checkpoint__in=checkpoints, completed_at__isnull=False, ).count() self.progress = round(done / total * 100) now = timezone.now() if done == 0: self.status = 'NOT_STARTED' self.completed_at = None elif done == total: self.status = 'COMPLETED' if not self.started_at: self.started_at = now if not self.completed_at: self.completed_at = now else: self.status = 'IN_PROGRESS' if not self.started_at: self.started_at = now self.completed_at = None if save: self.save() return True class UserCheckpointProgress(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='checkpoint_progress', ) checkpoint = models.ForeignKey( 'resources.ResourceCheckpoint', on_delete=models.CASCADE, related_name='user_progress', ) completed_at = models.DateTimeField(null=True, blank=True) class Meta: unique_together = [['user', 'checkpoint']] indexes = [models.Index(fields=['user', 'checkpoint'])] def __str__(self): state = 'done' if self.completed_at else 'open' return f'{self.user.email} - cp#{self.checkpoint_id} ({state})'