Skip to content

Commit acda6b2

Browse files
authored
Merge pull request #13 from open-craft/agrendalath/bb-9241-refactor-keys
feat: replace UUID with LearningPathKey
2 parents 8cc2884 + f9f2264 commit acda6b2

20 files changed

+467
-86
lines changed

.gitignore

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ pip-log.txt
2929
.coverage
3030
.coverage.*
3131
.tox
32+
.ruff_cache
3233
coverage.xml
3334
htmlcov/
35+
pii_report/
36+
default.db
3437

3538
# Virtual environments
3639
/venv/
@@ -63,5 +66,7 @@ docs/learning_paths.*.rst
6366
# Private requirements
6467
requirements/private.in
6568
requirements/private.txt
66-
.idea
67-
default.db
69+
70+
# IDEs
71+
.vscode/
72+
.idea/

CHANGELOG.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ Unreleased
1616

1717
*
1818

19+
0.3.0 - 2025-04-03
20+
******************
21+
22+
Changed
23+
=======
24+
25+
* Replaced Learning Path UUID with LearningPathKey.
26+
1927
0.2.3 - 2025-03-31
2028
******************
2129

@@ -38,7 +46,7 @@ Added
3846
Added
3947
=====
4048

41-
* Pathway progress API
49+
* Progress API
4250

4351
0.2.0 - 2024-01-23
4452
******************

docs/conf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,10 @@ def get_version(*file_paths):
516516

517517
# Example configuration for intersphinx: refer to the Python standard library.
518518
intersphinx_mapping = {
519-
"python": ("https://docs.python.org/3.8", None),
519+
"python": ("https://docs.python.org/3.11", None),
520520
"django": (
521-
"https://docs.djangoproject.com/en/3.2/",
522-
"https://docs.djangoproject.com/en/3.2/_objects/",
521+
"https://docs.djangoproject.com/en/4.2/",
522+
"https://docs.djangoproject.com/en/4.2/_objects/",
523523
),
524524
"model_utils": ("https://django-model-utils.readthedocs.io/en/latest/", None),
525525
}

learning_paths/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Learning Paths plugin.
33
"""
44

5-
__version__ = "0.2.3"
5+
__version__ = "0.3.0"

learning_paths/admin.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,16 @@ class LearningPathAdmin(admin.ModelAdmin):
104104
search_fields = [
105105
"slug",
106106
"display_name",
107+
"key",
107108
]
108109
list_display = (
109-
"uuid",
110+
"key",
110111
"slug",
111112
"display_name",
112113
"level",
113114
"duration_in_days",
114115
)
116+
readonly_fields = ("key",)
115117

116118
inlines = [
117119
LearningPathStepInline,
@@ -120,6 +122,12 @@ class LearningPathAdmin(admin.ModelAdmin):
120122
LearningPathGradingCriteriaInline,
121123
]
122124

125+
def get_readonly_fields(self, request, obj=None):
126+
"""Make key read-only only for existing objects."""
127+
if obj: # Editing an existing object.
128+
return self.readonly_fields
129+
return () # Allow all fields during creation.
130+
123131
def save_related(self, request, form, formsets, change):
124132
"""Save related objects and enroll users in the learning path."""
125133
super().save_related(request, form, formsets, change)
@@ -144,7 +152,7 @@ class EnrolledUsersAdmin(admin.ModelAdmin):
144152
search_fields = [
145153
"id",
146154
"user__username",
147-
"learning_path__uuid",
155+
"learning_path__key",
148156
"learning_path__slug",
149157
"learning_path__display_name",
150158
]

learning_paths/api/v1/serializers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class Meta:
7272

7373
# pylint: disable=abstract-method
7474
class LearningPathProgressSerializer(serializers.Serializer):
75-
learning_path_id = serializers.UUIDField()
75+
learning_path_key = serializers.CharField()
7676
progress = serializers.FloatField()
7777
required_completion = serializers.FloatField()
7878

@@ -82,7 +82,7 @@ class LearningPathGradeSerializer(serializers.Serializer):
8282
Serializer for learning path grade.
8383
"""
8484

85-
learning_path_id = serializers.UUIDField()
85+
learning_path_key = serializers.CharField()
8686
grade = serializers.FloatField()
8787
required_grade = serializers.FloatField()
8888

learning_paths/api/v1/tests/factories.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.contrib import auth
66
from factory.fuzzy import FuzzyText
77

8+
from learning_paths.keys import LearningPathKey
89
from learning_paths.models import (
910
LearningPath,
1011
LearningPathEnrollment,
@@ -30,22 +31,25 @@ class Meta:
3031
model = User
3132

3233

33-
class LearnerPathwayFactory(factory.django.DjangoModelFactory):
34+
class LearningPathFactory(factory.django.DjangoModelFactory):
3435
class Meta:
3536
model = LearningPath
3637

38+
key = factory.Sequence(
39+
lambda n: LearningPathKey.from_string(f"path-v1:test+number{n}+run+group")
40+
)
3741
uuid = factory.Faker("uuid4")
3842
display_name = FuzzyText()
3943
slug = FuzzyText()
4044
description = FuzzyText()
4145
sequential = False
4246

4347

44-
class LearnerPathGradingCriteriaFactory(factory.django.DjangoModelFactory):
48+
class LearningPathGradingCriteriaFactory(factory.django.DjangoModelFactory):
4549
class Meta:
4650
model = LearningPathGradingCriteria
4751

48-
learning_path = factory.SubFactory(LearnerPathwayFactory)
52+
learning_path = factory.SubFactory(LearningPathFactory)
4953
required_completion = 0.80
5054
required_grade = 0.75
5155

@@ -56,7 +60,7 @@ class LearningPathEnrollmentFactory(factory.django.DjangoModelFactory):
5660
"""
5761

5862
user = factory.SubFactory(UserFactory)
59-
learning_path = factory.SubFactory(LearnerPathwayFactory)
63+
learning_path = factory.SubFactory(LearningPathFactory)
6064
is_active = True
6165
enrolled_at = factory.LazyFunction(lambda: datetime.now(timezone.utc))
6266

learning_paths/api/v1/tests/test_serializers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def test_data(self):
4747
Tests LearningPathProgress serializer data.
4848
"""
4949
learning_path = LearningPath.objects.create(
50+
key="path-v1:test+test+test+test",
5051
uuid="817190bc-7bf1-4d95-aa43-bec5f58c2276",
5152
slug="learn-slug",
5253
display_name="My Test Learning Path",
@@ -55,7 +56,7 @@ def test_data(self):
5556
sequential=False,
5657
)
5758
progress_data = {
58-
"learning_path_id": learning_path.uuid,
59+
"learning_path_key": str(learning_path.key),
5960
"progress": 0.25,
6061
"required_completion": 0.80,
6162
}
@@ -70,6 +71,7 @@ def test_data(self):
7071
Tests LearningPathGrade serializer data.
7172
"""
7273
learning_path = LearningPath.objects.create(
74+
key="path-v1:OpenedX+DemoX+DemoRun+DemoGroup",
7375
uuid="817190bc-7bf1-4d95-aa43-bec5f58c2276",
7476
slug="learn-slug",
7577
display_name="My Test Learning Path",
@@ -78,7 +80,7 @@ def test_data(self):
7880
sequential=False,
7981
)
8082
grade_data = {
81-
"learning_path_id": learning_path.uuid,
83+
"learning_path_key": str(learning_path.key),
8284
"grade": 0.25,
8385
"required_grade": 0.80,
8486
}

learning_paths/api/v1/tests/test_views.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
LearningPathProgressSerializer,
1212
)
1313
from learning_paths.api.v1.tests.factories import (
14-
LearnerPathGradingCriteriaFactory,
15-
LearnerPathwayFactory,
1614
LearningPathEnrollmentFactory,
15+
LearningPathFactory,
16+
LearningPathGradingCriteriaFactory,
1717
UserFactory,
1818
)
1919
from learning_paths.api.v1.views import (
@@ -26,7 +26,7 @@
2626
class LearningPathAsProgramTests(APITestCase):
2727
def setUp(self):
2828
super().setUp()
29-
self.learning_paths = LearnerPathwayFactory.create_batch(5)
29+
self.learning_paths = LearningPathFactory.create_batch(5)
3030
self.user = UserFactory()
3131
self.client.force_authenticate(user=self.user)
3232

@@ -51,8 +51,8 @@ def setUp(self):
5151
super().setUp()
5252
self.user = UserFactory()
5353
self.client.force_authenticate(user=self.user)
54-
self.learning_path = LearnerPathwayFactory.create()
55-
self.grading_criteria = LearnerPathGradingCriteriaFactory.create(
54+
self.learning_path = LearningPathFactory.create()
55+
self.grading_criteria = LearningPathGradingCriteriaFactory.create(
5656
learning_path=self.learning_path,
5757
required_completion=0.80,
5858
required_grade=0.75,
@@ -65,16 +65,16 @@ def test_learning_path_progress_success(
6565
"""
6666
Test retrieving progress for a learning path.
6767
"""
68-
url = reverse("learning-path-progress", args=[self.learning_path.uuid])
68+
url = reverse("learning-path-progress", args=[self.learning_path.key])
6969
request = APIRequestFactory().get(url, format="json")
7070
view = LearningPathUserProgressView.as_view()
7171
force_authenticate(request, user=self.user)
72-
response = view(request, learning_path_uuid=self.learning_path.uuid)
72+
response = view(request, learning_path_key_str=str(self.learning_path.key))
7373

7474
self.assertEqual(response.status_code, status.HTTP_200_OK)
7575

7676
expected_data = {
77-
"learning_path_id": str(self.learning_path.uuid),
77+
"learning_path_key": str(self.learning_path.key),
7878
"progress": 0.75,
7979
"required_completion": 0.80,
8080
}
@@ -88,8 +88,8 @@ def setUp(self) -> None:
8888
super().setUp()
8989
self.staff_user = UserFactory(is_staff=True)
9090
self.client.force_authenticate(user=self.staff_user)
91-
self.learning_path = LearnerPathwayFactory.create()
92-
self.grading_criteria = LearnerPathGradingCriteriaFactory.create(
91+
self.learning_path = LearningPathFactory.create()
92+
self.grading_criteria = LearningPathGradingCriteriaFactory.create(
9393
learning_path=self.learning_path,
9494
required_completion=0.80,
9595
required_grade=0.75,
@@ -100,7 +100,7 @@ def test_learning_path_grade_grading_criteria_not_found(self):
100100
Test that the grade view returns 404 if grading criteria are not found.
101101
"""
102102
self.grading_criteria.delete()
103-
url = reverse("learning-path-grade", args=[self.learning_path.uuid])
103+
url = reverse("learning-path-grade", args=[self.learning_path.key])
104104
response = self.client.get(url)
105105

106106
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -120,7 +120,7 @@ def test_learning_path_grade_success(
120120
"""
121121
Test retrieving grade for a learning path.
122122
"""
123-
url = reverse("learning-path-grade", args=[self.learning_path.uuid])
123+
url = reverse("learning-path-grade", args=[self.learning_path.key])
124124
response = self.client.get(url)
125125

126126
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -134,8 +134,8 @@ def setUp(self) -> None:
134134
self.staff = UserFactory(is_staff=True)
135135
self.learner = UserFactory()
136136
self.another_learner = UserFactory()
137-
self.learning_path = LearnerPathwayFactory.create()
138-
self.url = f"/api/learning_paths/v1/{self.learning_path.uuid}/enrollments/"
137+
self.learning_path = LearningPathFactory.create()
138+
self.url = f"/api/learning_paths/v1/{self.learning_path.key}/enrollments/"
139139

140140
def test_get_with_username_for_staff(self):
141141
"""
@@ -289,9 +289,7 @@ def test_enrollment_returns_404_for_invalid_user_or_learning_path(self):
289289
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
290290

291291
# Test invalid learning_path_id
292-
url = reverse(
293-
"learning-path-enrollments", args=["2ac8a3cc-e492-4ce9-88a3-cce4922ce9df"]
294-
)
292+
url = reverse("learning-path-enrollments", args=["path-v1:this+does+not+exist"])
295293
response = self.client.post(url)
296294
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
297295

@@ -490,8 +488,8 @@ def setUp(self):
490488
self.admin_user = UserFactory(is_staff=True, is_superuser=True)
491489
self.client.force_authenticate(user=self.admin_user)
492490

493-
self.learning_path1 = LearnerPathwayFactory()
494-
self.learning_path2 = LearnerPathwayFactory()
491+
self.learning_path1 = LearningPathFactory()
492+
self.learning_path2 = LearningPathFactory()
495493

496494
self.user1 = UserFactory(email="user1@example.com")
497495
self.user2 = UserFactory(email="user2@example.com")
@@ -503,11 +501,11 @@ def _call_api(self, payload):
503501
def test_bulk_enrollment_success(self):
504502
"""
505503
GIVEN valid payload from staff user
506-
WHEN then uuids and emails are valid
504+
WHEN then keys and emails are valid
507505
THEN create necessary enrollments and enrollment allowed objects.
508506
"""
509507
payload = {
510-
"learning_paths": f"{self.learning_path1.uuid},{self.learning_path2.uuid}",
508+
"learning_paths": f"{self.learning_path1.key},{self.learning_path2.key}",
511509
"emails": "user1@example.com,user2@example.com,new_user@example.com",
512510
}
513511
response = self._call_api(payload)
@@ -549,11 +547,11 @@ def test_bulk_enrollment_success(self):
549547
def test_bulk_enrollment_with_invalid_learning_path(self):
550548
"""
551549
GIVEN valid payload from staff user
552-
WHEN the learning path uuid is invalid
550+
WHEN the learning path key is invalid
553551
THEN no enrollments are created.
554552
"""
555553
payload = {
556-
"learning_paths": "invalid-path-uuid",
554+
"learning_paths": "invalid-path-key",
557555
"emails": "user1@example.com,user2@example.com",
558556
}
559557
response = self._call_api(payload)
@@ -569,7 +567,7 @@ def test_bulk_enrollment_with_invalid_and_valid_emails(self):
569567
THEN no enrollments are created for the invalid email.
570568
"""
571569
payload = {
572-
"learning_paths": f"{self.learning_path1.uuid}",
570+
"learning_paths": f"{self.learning_path1.key}",
573571
"emails": "user1@example.com,invalid_email",
574572
}
575573
response = self._call_api(payload)
@@ -600,7 +598,7 @@ def test_bulk_enrollment_unauthenticated(self):
600598
"""
601599
self.client.logout()
602600
payload = {
603-
"learning_paths": f"{self.learning_path1.uuid}",
601+
"learning_paths": f"{self.learning_path1.key}",
604602
"emails": "user1@example.com",
605603
}
606604
# Un-authenticates
@@ -623,7 +621,7 @@ def test_bulk_enrollment_returned_counts_reflect_only_new_ones(self):
623621
)
624622

625623
payload = {
626-
"learning_paths": f"{self.learning_path1.uuid}",
624+
"learning_paths": f"{self.learning_path1.key}",
627625
"emails": self.user1.email,
628626
}
629627

@@ -644,7 +642,7 @@ def test_re_enrollment(self):
644642
)
645643

646644
payload = {
647-
"learning_paths": f"{self.learning_path1.uuid}",
645+
"learning_paths": f"{self.learning_path1.key}",
648646
"emails": self.user1.email,
649647
}
650648

0 commit comments

Comments
 (0)