Skip to content

Commit 4715469

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

File tree

5 files changed

+163
-2
lines changed

5 files changed

+163
-2
lines changed

learning_paths/api/v1/serializers.py

Lines changed: 44 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,46 @@ 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(
109+
source="requiredskill_set", many=True, read_only=True
110+
)
111+
acquired_skills = SkillSerializer(
112+
source="acquiredskill_set", many=True, read_only=True
113+
)
114+
115+
class Meta:
116+
model = LearningPath
117+
fields = [
118+
"uuid",
119+
"slug",
120+
"display_name",
121+
"subtitle",
122+
"description",
123+
"image_url",
124+
"level",
125+
"duration_in_days",
126+
"sequential",
127+
"steps",
128+
"required_skills",
129+
"acquired_skills",
130+
]

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: 54 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,54 @@ 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(
139+
learning_path=lp, order=1, course_key="course-v1:edX+DemoX+Demo_Course"
140+
)
141+
LearningPathStepFactory.create(
142+
learning_path=lp,
143+
order=2,
144+
course_key="course-v1:edX+DemoX+Another_Course",
145+
)
146+
RequiredSkillFactory.create(learning_path=lp)
147+
AcquiredSkillFactory.create(learning_path=lp)
148+
149+
def test_learning_path_list(self):
150+
"""
151+
Test that the list endpoint returns all learning paths with basic fields.
152+
"""
153+
url = reverse("learning-path-list")
154+
response = self.client.get(url, format="json")
155+
self.assertEqual(response.status_code, status.HTTP_200_OK)
156+
self.assertEqual(len(response.data), len(self.learning_paths))
157+
first_item = response.data[0]
158+
self.assertIn("uuid", first_item)
159+
self.assertIn("slug", first_item)
160+
self.assertIn("display_name", first_item)
161+
162+
def test_learning_path_retrieve(self):
163+
"""
164+
Test that the retrieve endpoint returns the details of a learning path,
165+
including steps and associated skills.
166+
"""
167+
lp = self.learning_paths[0]
168+
url = reverse("learning-path-detail", args=[lp.uuid])
169+
response = self.client.get(url, format="json")
170+
self.assertEqual(response.status_code, status.HTTP_200_OK)
171+
self.assertIn("steps", response.data)
172+
self.assertIn("required_skills", response.data)
173+
self.assertIn("acquired_skills", response.data)
174+
if response.data["steps"]:
175+
first_step = response.data["steps"][0]
176+
self.assertIn("order", first_step)
177+
self.assertIn("course_key", first_step)
178+
self.assertIn("relative_due_date_in_days", first_step)
179+
self.assertIn("weight", first_step)

learning_paths/api/v1/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
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(r"learning-paths", LearningPathViewSet, basename="learning-path")
1618

1719
urlpatterns = router.urls + [
1820
path(

learning_paths/api/v1/views.py

Lines changed: 18 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,19 @@ 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+
118+
queryset = LearningPath.objects.all()
119+
permission_classes = (IsAuthenticated,)
120+
pagination_class = PageNumberPagination
121+
122+
def get_serializer_class(self):
123+
if self.action == "list":
124+
return LearningPathListSerializer
125+
return LearningPathDetailSerializer

0 commit comments

Comments
 (0)