Spaces:
Sleeping
Sleeping
| 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})' | |