Skip to content

Commit f71b778

Browse files
committed
feat: Add list and retrieve api for learning paths
1 parent 0d2cd10 commit f71b778

File tree

5 files changed

+154
-2
lines changed

5 files changed

+154
-2
lines changed

learning_paths/api/v1/serializers.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from rest_framework import serializers
66

7-
from learning_paths.models import LearningPath
7+
from learning_paths.models import LearningPath, LearningPathStep, Skill
88

99
DEFAULT_STATUS = "active"
1010
IMAGE_WIDTH = 1440
@@ -85,3 +85,42 @@ class LearningPathGradeSerializer(serializers.Serializer):
8585
learning_path_id = serializers.UUIDField()
8686
grade = serializers.FloatField()
8787
required_grade = serializers.FloatField()
88+
89+
90+
class LearningPathListSerializer(serializers.ModelSerializer):
91+
class Meta:
92+
model = LearningPath
93+
fields = ['uuid', 'slug', 'display_name', 'sequential']
94+
95+
96+
class LearningPathStepSerializer(serializers.ModelSerializer):
97+
class Meta:
98+
model = LearningPathStep
99+
fields = ['order', 'course_key', 'relative_due_date_in_days', 'weight']
100+
101+
class SkillSerializer(serializers.ModelSerializer):
102+
class Meta:
103+
model = Skill
104+
fields = ['id', 'display_name']
105+
106+
class LearningPathDetailSerializer(serializers.ModelSerializer):
107+
steps = LearningPathStepSerializer(many=True, read_only=True)
108+
required_skills = SkillSerializer(source='requiredskill_set', many=True, read_only=True)
109+
acquired_skills = SkillSerializer(source='acquiredskill_set', many=True, read_only=True)
110+
111+
class Meta:
112+
model = LearningPath
113+
fields = [
114+
'uuid',
115+
'slug',
116+
'display_name',
117+
'subtitle',
118+
'description',
119+
'image_url',
120+
'level',
121+
'duration_in_days',
122+
'sequential',
123+
'steps',
124+
'required_skills',
125+
'acquired_skills',
126+
]

learning_paths/api/v1/tests/factories.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
from django.contrib import auth
44
from factory.fuzzy import FuzzyText
55

6-
from learning_paths.models import LearningPath, LearningPathGradingCriteria
6+
from learning_paths.models import (
7+
AcquiredSkill,
8+
LearningPath,
9+
LearningPathGradingCriteria,
10+
LearningPathStep,
11+
RequiredSkill,
12+
Skill,
13+
)
14+
715

816
User = auth.get_user_model()
917

@@ -43,3 +51,39 @@ class Meta:
4351
learning_path = factory.SubFactory(LearnerPathwayFactory)
4452
required_completion = 0.80
4553
required_grade = 0.75
54+
55+
56+
class LearningPathStepFactory(factory.django.DjangoModelFactory):
57+
class Meta:
58+
model = LearningPathStep
59+
60+
learning_path = factory.SubFactory(LearnerPathwayFactory)
61+
course_key = "course-v1:edX+DemoX+Demo_Course"
62+
relative_due_date_in_days = factory.Faker("random_int", min=1, max=30)
63+
order = factory.Sequence(lambda n: n + 1)
64+
weight = 1
65+
66+
67+
class SkillFactory(factory.django.DjangoModelFactory):
68+
class Meta:
69+
model = Skill
70+
71+
display_name = factory.Faker("word")
72+
73+
74+
class RequiredSkillFactory(factory.django.DjangoModelFactory):
75+
class Meta:
76+
model = RequiredSkill
77+
78+
learning_path = factory.SubFactory(LearnerPathwayFactory)
79+
skill = factory.SubFactory(SkillFactory)
80+
level = factory.Faker("random_int", min=1, max=5)
81+
82+
83+
class AcquiredSkillFactory(factory.django.DjangoModelFactory):
84+
class Meta:
85+
model = AcquiredSkill
86+
87+
learning_path = factory.SubFactory(LearnerPathwayFactory)
88+
skill = factory.SubFactory(SkillFactory)
89+
level = factory.Faker("random_int", min=1, max=5)

learning_paths/api/v1/tests/test_views.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
LearningPathProgressSerializer,
1111
)
1212
from learning_paths.api.v1.tests.factories import (
13+
AcquiredSkillFactory,
1314
LearnerPathGradingCriteriaFactory,
1415
LearnerPathwayFactory,
16+
LearningPathStepFactory,
17+
RequiredSkillFactory,
1518
UserFactory,
1619
)
1720
from learning_paths.api.v1.views import (
@@ -123,3 +126,48 @@ def test_learning_path_grade_success(
123126
self.assertEqual(response.status_code, status.HTTP_200_OK)
124127
self.assertEqual(response.data["grade"], 0.85)
125128
self.assertTrue(response.data["required_grade"], 0.75)
129+
130+
131+
class LearningPathViewSetTests(APITestCase):
132+
def setUp(self) -> None:
133+
super().setUp()
134+
self.user = UserFactory()
135+
self.client.force_authenticate(user=self.user)
136+
self.learning_paths = LearnerPathwayFactory.create_batch(3)
137+
for lp in self.learning_paths:
138+
LearningPathStepFactory.create(learning_path=lp, order=1, course_key="course-v1:edX+DemoX+Demo_Course")
139+
LearningPathStepFactory.create(learning_path=lp, order=2, course_key="course-v1:edX+DemoX+Another_Course")
140+
RequiredSkillFactory.create(learning_path=lp)
141+
AcquiredSkillFactory.create(learning_path=lp)
142+
143+
def test_learning_path_list(self):
144+
"""
145+
Test that the list endpoint returns all learning paths with basic fields.
146+
"""
147+
url = reverse("learning-path-list")
148+
response = self.client.get(url, format="json")
149+
self.assertEqual(response.status_code, status.HTTP_200_OK)
150+
self.assertEqual(len(response.data), len(self.learning_paths))
151+
first_item = response.data[0]
152+
self.assertIn("uuid", first_item)
153+
self.assertIn("slug", first_item)
154+
self.assertIn("display_name", first_item)
155+
156+
def test_learning_path_retrieve(self):
157+
"""
158+
Test that the retrieve endpoint returns the details of a learning path,
159+
including steps and associated skills.
160+
"""
161+
lp = self.learning_paths[0]
162+
url = reverse("learning-path-detail", args=[lp.uuid])
163+
response = self.client.get(url, format="json")
164+
self.assertEqual(response.status_code, status.HTTP_200_OK)
165+
self.assertIn("steps", response.data)
166+
self.assertIn("required_skills", response.data)
167+
self.assertIn("acquired_skills", response.data)
168+
if response.data["steps"]:
169+
first_step = response.data["steps"][0]
170+
self.assertIn("order", first_step)
171+
self.assertIn("course_key", first_step)
172+
self.assertIn("relative_due_date_in_days", first_step)
173+
self.assertIn("weight", first_step)

learning_paths/api/v1/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
LearningPathAsProgramViewSet,
88
LearningPathUserGradeView,
99
LearningPathUserProgressView,
10+
LearningPathViewSet,
1011
)
1112

1213
router = routers.SimpleRouter()
1314
router.register(
1415
r"programs", LearningPathAsProgramViewSet, basename="learning-path-as-program"
1516
)
17+
router.register(
18+
r"learning-paths", LearningPathViewSet, basename="learning-path"
19+
)
1620

1721
urlpatterns = router.urls + [
1822
path(

learning_paths/api/v1/views.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
from learning_paths.api.v1.serializers import (
1515
LearningPathAsProgramSerializer,
16+
LearningPathDetailSerializer,
1617
LearningPathGradeSerializer,
18+
LearningPathListSerializer,
1719
LearningPathProgressSerializer,
1820
)
1921
from learning_paths.models import LearningPath
@@ -105,3 +107,18 @@ def get(self, request, learning_path_uuid):
105107
if serializer.is_valid():
106108
return Response(serializer.data, status=status.HTTP_200_OK)
107109
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
110+
111+
112+
class LearningPathViewSet(viewsets.ReadOnlyModelViewSet):
113+
"""
114+
ViewSet for listing all learning paths and retrieving a specific learning path's details,
115+
including steps and associated skills.
116+
"""
117+
queryset = LearningPath.objects.all()
118+
permission_classes = (IsAuthenticated,)
119+
pagination_class = PageNumberPagination
120+
121+
def get_serializer_class(self):
122+
if self.action == 'list':
123+
return LearningPathListSerializer
124+
return LearningPathDetailSerializer

0 commit comments

Comments
 (0)