Skip to content

Commit

Permalink
feat: Add list and retrieve api for learning paths
Browse files Browse the repository at this point in the history
  • Loading branch information
pkulkark committed Feb 18, 2025
1 parent 0d2cd10 commit 4f764f7
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 2 deletions.
47 changes: 46 additions & 1 deletion learning_paths/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from rest_framework import serializers

from learning_paths.models import LearningPath
from learning_paths.models import LearningPath, LearningPathStep, Skill

DEFAULT_STATUS = "active"
IMAGE_WIDTH = 1440
Expand Down Expand Up @@ -85,3 +85,48 @@ class LearningPathGradeSerializer(serializers.Serializer):
learning_path_id = serializers.UUIDField()
grade = serializers.FloatField()
required_grade = serializers.FloatField()


class LearningPathListSerializer(serializers.ModelSerializer):
class Meta:
model = LearningPath
fields = ["uuid", "slug", "display_name", "sequential"]


class LearningPathStepSerializer(serializers.ModelSerializer):
class Meta:
model = LearningPathStep
fields = ["order", "course_key", "relative_due_date_in_days", "weight"]


class SkillSerializer(serializers.ModelSerializer):
class Meta:
model = Skill
fields = ["id", "display_name"]


class LearningPathDetailSerializer(serializers.ModelSerializer):
steps = LearningPathStepSerializer(many=True, read_only=True)
required_skills = SkillSerializer(
source="requiredskill_set", many=True, read_only=True
)
acquired_skills = SkillSerializer(
source="acquiredskill_set", many=True, read_only=True
)

class Meta:
model = LearningPath
fields = [
"uuid",
"slug",
"display_name",
"subtitle",
"description",
"image_url",
"level",
"duration_in_days",
"sequential",
"steps",
"required_skills",
"acquired_skills",
]
46 changes: 45 additions & 1 deletion learning_paths/api/v1/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
from django.contrib import auth
from factory.fuzzy import FuzzyText

from learning_paths.models import LearningPath, LearningPathGradingCriteria
from learning_paths.models import (
AcquiredSkill,
LearningPath,
LearningPathGradingCriteria,
LearningPathStep,
RequiredSkill,
Skill,
)


User = auth.get_user_model()

Expand Down Expand Up @@ -43,3 +51,39 @@ class Meta:
learning_path = factory.SubFactory(LearnerPathwayFactory)
required_completion = 0.80
required_grade = 0.75


class LearningPathStepFactory(factory.django.DjangoModelFactory):
class Meta:
model = LearningPathStep

learning_path = factory.SubFactory(LearnerPathwayFactory)
course_key = "course-v1:edX+DemoX+Demo_Course"
relative_due_date_in_days = factory.Faker("random_int", min=1, max=30)
order = factory.Sequence(lambda n: n + 1)
weight = 1


class SkillFactory(factory.django.DjangoModelFactory):
class Meta:
model = Skill

display_name = factory.Faker("word")


class RequiredSkillFactory(factory.django.DjangoModelFactory):
class Meta:
model = RequiredSkill

learning_path = factory.SubFactory(LearnerPathwayFactory)
skill = factory.SubFactory(SkillFactory)
level = factory.Faker("random_int", min=1, max=5)


class AcquiredSkillFactory(factory.django.DjangoModelFactory):
class Meta:
model = AcquiredSkill

learning_path = factory.SubFactory(LearnerPathwayFactory)
skill = factory.SubFactory(SkillFactory)
level = factory.Faker("random_int", min=1, max=5)
54 changes: 54 additions & 0 deletions learning_paths/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
LearningPathProgressSerializer,
)
from learning_paths.api.v1.tests.factories import (
AcquiredSkillFactory,
LearnerPathGradingCriteriaFactory,
LearnerPathwayFactory,
LearningPathStepFactory,
RequiredSkillFactory,
UserFactory,
)
from learning_paths.api.v1.views import (
Expand Down Expand Up @@ -123,3 +126,54 @@ def test_learning_path_grade_success(
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["grade"], 0.85)
self.assertTrue(response.data["required_grade"], 0.75)


class LearningPathViewSetTests(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = UserFactory()
self.client.force_authenticate(user=self.user)
self.learning_paths = LearnerPathwayFactory.create_batch(3)
for lp in self.learning_paths:
LearningPathStepFactory.create(
learning_path=lp, order=1, course_key="course-v1:edX+DemoX+Demo_Course"
)
LearningPathStepFactory.create(
learning_path=lp,
order=2,
course_key="course-v1:edX+DemoX+Another_Course",
)
RequiredSkillFactory.create(learning_path=lp)
AcquiredSkillFactory.create(learning_path=lp)

def test_learning_path_list(self):
"""
Test that the list endpoint returns all learning paths with basic fields.
"""
url = reverse("learning-path-list")
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), len(self.learning_paths))
first_item = response.data[0]
self.assertIn("uuid", first_item)
self.assertIn("slug", first_item)
self.assertIn("display_name", first_item)

def test_learning_path_retrieve(self):
"""
Test that the retrieve endpoint returns the details of a learning path,
including steps and associated skills.
"""
lp = self.learning_paths[0]
url = reverse("learning-path-detail", args=[lp.uuid])
response = self.client.get(url, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("steps", response.data)
self.assertIn("required_skills", response.data)
self.assertIn("acquired_skills", response.data)
if response.data["steps"]:
first_step = response.data["steps"][0]
self.assertIn("order", first_step)
self.assertIn("course_key", first_step)
self.assertIn("relative_due_date_in_days", first_step)
self.assertIn("weight", first_step)
2 changes: 2 additions & 0 deletions learning_paths/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
LearningPathAsProgramViewSet,
LearningPathUserGradeView,
LearningPathUserProgressView,
LearningPathViewSet,
)

router = routers.SimpleRouter()
router.register(
r"programs", LearningPathAsProgramViewSet, basename="learning-path-as-program"
)
router.register(r"learning-paths", LearningPathViewSet, basename="learning-path")

urlpatterns = router.urls + [
path(
Expand Down
18 changes: 18 additions & 0 deletions learning_paths/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

from learning_paths.api.v1.serializers import (
LearningPathAsProgramSerializer,
LearningPathDetailSerializer,
LearningPathGradeSerializer,
LearningPathListSerializer,
LearningPathProgressSerializer,
)
from learning_paths.models import LearningPath
Expand Down Expand Up @@ -105,3 +107,19 @@ def get(self, request, learning_path_uuid):
if serializer.is_valid():
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class LearningPathViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet for listing all learning paths and retrieving a specific learning path's details,
including steps and associated skills.
"""

queryset = LearningPath.objects.all()
permission_classes = (IsAuthenticated,)
pagination_class = PageNumberPagination

def get_serializer_class(self):
if self.action == "list":
return LearningPathListSerializer
return LearningPathDetailSerializer

0 comments on commit 4f764f7

Please sign in to comment.