Skip to content

Commit 9bd6ab8

Browse files
committed
feat: return enrollment date in the API instead of a boolean
1 parent f189025 commit 9bd6ab8

File tree

6 files changed

+36
-36
lines changed

6 files changed

+36
-36
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Changed
2929
=======
3030

3131
* The Learning Paths API includes start and end dates for its steps.
32+
* Return enrollment date in the API instead of a boolean.
3233

3334
0.3.3 - 2025-05-23
3435
******************

learning_paths/api/v1/serializers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class LearningPathListSerializer(serializers.ModelSerializer):
105105

106106
steps = LearningPathStepSerializer(many=True, read_only=True)
107107
required_completion = serializers.FloatField(source="grading_criteria.required_completion", read_only=True)
108-
is_enrolled = serializers.SerializerMethodField()
108+
enrollment_date = serializers.SerializerMethodField()
109109
invite_only = serializers.BooleanField()
110110
image = serializers.ImageField(read_only=True)
111111

@@ -118,17 +118,17 @@ class Meta:
118118
"sequential",
119119
"steps",
120120
"required_completion",
121-
"is_enrolled",
121+
"enrollment_date",
122122
"invite_only",
123123
]
124124

125-
def get_is_enrolled(self, obj):
125+
def get_enrollment_date(self, obj):
126126
"""
127127
Check if the current user is enrolled in this learning path.
128128
"""
129-
if hasattr(obj, "is_enrolled"):
130-
return obj.is_enrolled
131-
return False
129+
if hasattr(obj, "enrollment_date"):
130+
return obj.enrollment_date
131+
return None
132132

133133

134134
class SkillSerializer(serializers.ModelSerializer):

learning_paths/api/v1/tests/test_serializers.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from learning_paths.tests.factories import (
1313
LearningPathEnrollmentFactory,
1414
LearningPathFactory,
15-
UserFactory,
1615
)
1716

1817

@@ -105,26 +104,24 @@ def test_list_serializer():
105104
"sequential": False,
106105
"steps": [],
107106
"required_completion": 0.8,
108-
"is_enrolled": False,
107+
"enrollment_date": None,
109108
}
110109
assert dict(serializer.data) == expected
111110

112111

113112
@pytest.mark.django_db
114113
@pytest.mark.parametrize("is_enrolled", [True, False], ids=["enrolled", "not_enrolled"])
115-
def test_list_serializer_enrollment(is_enrolled):
114+
def test_list_serializer_enrollment(user, learning_path, is_enrolled):
116115
"""
117-
Tests LearningPathListSerializer shows is_enrolled as True when a user is enrolled.
116+
Tests LearningPathListSerializer shows enrollment_date when a user is enrolled.
118117
"""
119-
user = UserFactory()
120-
learning_path = LearningPathFactory(invite_only=False)
121-
LearningPathEnrollmentFactory(user=user, learning_path=learning_path, is_active=is_enrolled)
118+
enrollment = LearningPathEnrollmentFactory(user=user, learning_path=learning_path, is_active=is_enrolled)
122119

123120
# Get the annotated learning path with the enrollment status.
124121
learning_path = learning_path.__class__.objects.get_paths_visible_to_user(user).get(key=learning_path.key)
125122

126123
serializer = LearningPathListSerializer(learning_path)
127-
assert serializer.data["is_enrolled"] is is_enrolled
124+
assert serializer.data["enrollment_date"] == (enrollment.created if is_enrolled else None)
128125

129126

130127
@pytest.mark.django_db
@@ -146,7 +143,7 @@ def test_detail_serializer():
146143
"required_skills": [],
147144
"acquired_skills": [],
148145
"steps": [],
149-
"is_enrolled": False,
146+
"enrollment_date": None,
150147
"required_completion": 0.8,
151148
}
152149
serializer = LearningPathDetailSerializer(learning_path)

learning_paths/api/v1/tests/test_views.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ def test_learning_path_list(self, authenticated_client, learning_paths_with_step
219219
assert "key" in first_item
220220
assert "display_name" in first_item
221221
assert "steps" in first_item
222-
assert "is_enrolled" in first_item
223-
assert first_item["is_enrolled"] is False
222+
assert "enrollment_date" in first_item
223+
assert first_item["enrollment_date"] is None
224224

225225
def test_learning_path_retrieve(self, authenticated_client, learning_paths_with_steps):
226226
"""Test that the retrieve endpoint returns the details of a learning path."""
@@ -232,8 +232,8 @@ def test_learning_path_retrieve(self, authenticated_client, learning_paths_with_
232232
assert "steps" in response.data
233233
assert "required_skills" in response.data
234234
assert "acquired_skills" in response.data
235-
assert "is_enrolled" in response.data
236-
assert response.data["is_enrolled"] is False
235+
assert "enrollment_date" in response.data
236+
assert response.data["enrollment_date"] is None
237237

238238
if response.data["steps"]:
239239
first_step = response.data["steps"][0]
@@ -264,17 +264,17 @@ def test_learning_path_list_with_enrollment(self, authenticated_client, active_e
264264
assert response.status_code == status.HTTP_200_OK
265265
assert len(response.data) == 1
266266
first_item = response.data[0]
267-
assert "is_enrolled" in first_item
268-
assert first_item["is_enrolled"] is True
267+
assert "enrollment_date" in first_item
268+
assert first_item["enrollment_date"] is not None
269269

270270
def test_learning_path_retrieve_with_enrollment(self, authenticated_client, active_enrollment, learning_path, user):
271271
"""Test that the retrieve endpoint returns the details of a learning path with enrollment status."""
272272
url = reverse("learning-path-detail", args=[learning_path.key])
273273
response = authenticated_client.get(url)
274274

275275
assert response.status_code == status.HTTP_200_OK
276-
assert "is_enrolled" in response.data
277-
assert response.data["is_enrolled"] is True
276+
assert "enrollment_date" in response.data
277+
assert response.data["enrollment_date"] is not None
278278

279279
def test_learning_path_retrieve_with_inactive_enrollment(
280280
self, authenticated_client, inactive_enrollment, learning_path, user
@@ -284,8 +284,8 @@ def test_learning_path_retrieve_with_inactive_enrollment(
284284
response = authenticated_client.get(url)
285285

286286
assert response.status_code == status.HTTP_200_OK
287-
assert "is_enrolled" in response.data
288-
assert response.data["is_enrolled"] is False
287+
assert "enrollment_date" in response.data
288+
assert response.data["enrollment_date"] is None
289289

290290
def test_invite_only_learning_paths_hidden_from_non_enrolled_users(
291291
self, authenticated_client, learning_path_with_invite_only, learning_path

learning_paths/api/v1/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ def _get_enrolled_learning_path(self, learning_path_key_str: str) -> LearningPat
516516
:raises: Http404 if the learning path is not found or the user does not have access.
517517
"""
518518
return get_object_or_404(
519-
LearningPath.objects.get_paths_visible_to_user(self.request.user).filter(is_enrolled=True),
519+
LearningPath.objects.get_paths_visible_to_user(self.request.user).filter(enrollment_date__isnull=False),
520520
key=learning_path_key_str,
521521
)
522522

learning_paths/models.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.core.exceptions import ValidationError
1313
from django.core.validators import MaxValueValidator, MinValueValidator
1414
from django.db import models
15-
from django.db.models import Exists, OuterRef, Q
15+
from django.db.models import OuterRef, Q
1616
from django.utils.translation import gettext_lazy as _
1717
from model_utils import FieldTracker
1818
from model_utils.models import TimeStampedModel
@@ -38,27 +38,29 @@ class LearningPathManager(models.Manager):
3838

3939
def get_paths_visible_to_user(self, user: User) -> models.QuerySet:
4040
"""
41-
Return only learning paths that should be visible to the given user with enrollment status.
41+
Return only learning paths that should be visible to the given user with an enrollment date.
4242
4343
For staff users: all learning paths.
4444
For non-staff: non-invite-only paths or invite-only paths they're enrolled in.
4545
46-
Each learning path in the queryset is annotated with `is_enrolled` indicating
47-
whether the user has an active enrollment in that learning path.
46+
Each learning path in the queryset is annotated with `enrollment_date` indicating
47+
the date when the user enrolled in that learning path (None if not enrolled).
48+
Results are ordered by enrollment date (the most recent first), with non-enrolled paths at the end.
4849
"""
4950
queryset = self.get_queryset()
5051

51-
# Annotate each path with whether the user is enrolled.
52-
enrollment_exists = LearningPathEnrollment.objects.filter(
52+
# Annotate each path with the enrollment date.
53+
enrollment_subquery = LearningPathEnrollment.objects.filter(
5354
learning_path=OuterRef("pk"), user=user, is_active=True
54-
)
55-
queryset = queryset.annotate(is_enrolled=Exists(enrollment_exists))
55+
).values("created")[:1]
56+
queryset = queryset.annotate(enrollment_date=models.Subquery(enrollment_subquery))
5657

5758
# Apply visibility filtering based on the user role.
5859
if not user.is_staff:
59-
queryset = queryset.filter(Q(invite_only=False) | Q(is_enrolled=True))
60+
queryset = queryset.filter(Q(invite_only=False) | Q(enrollment_date__isnull=False))
6061

61-
return queryset
62+
# Order by enrollment date (the most recent first), with null values at the end.
63+
return queryset.order_by(models.F("enrollment_date").desc(nulls_last=True))
6264

6365

6466
class LearningPath(TimeStampedModel):

0 commit comments

Comments
 (0)