File size: 3,260 Bytes
ffd36e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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})'