learning-paths-plugin 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/CHANGELOG.rst +18 -0
  2. {learning_paths_plugin-0.3.0/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.1}/PKG-INFO +19 -1
  3. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/__init__.py +1 -1
  4. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/serializers.py +100 -1
  5. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/urls.py +2 -0
  6. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/views.py +27 -0
  7. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/compat.py +17 -0
  8. learning_paths_plugin-0.3.1/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +17 -0
  9. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/models.py +15 -10
  10. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1/learning_paths_plugin.egg-info}/PKG-INFO +19 -1
  11. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/SOURCES.txt +1 -0
  12. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/tests/test_models.py +8 -0
  13. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/LICENSE.txt +0 -0
  14. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/MANIFEST.in +0 -0
  15. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/README.rst +0 -0
  16. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/admin.py +0 -0
  17. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/__init__.py +0 -0
  18. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/urls.py +0 -0
  19. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/__init__.py +0 -0
  20. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/filters.py +0 -0
  21. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/permissions.py +0 -0
  22. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/api/v1/utils.py +0 -0
  23. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/apps.py +0 -0
  24. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/keys.py +0 -0
  25. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0001_initial.py +0 -0
  26. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
  27. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
  28. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
  29. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
  30. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0006_enrollment_models.py +0 -0
  31. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
  32. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/migrations/__init__.py +0 -0
  33. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/receivers.py +0 -0
  34. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/settings.py +0 -0
  35. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/templates/learning_paths/base.html +0 -0
  36. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths/urls.py +0 -0
  37. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
  38. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
  39. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
  40. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/requires.txt +0 -0
  41. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/learning_paths_plugin.egg-info/top_level.txt +0 -0
  42. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/pyproject.toml +0 -0
  43. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/requirements/base.in +0 -0
  44. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/requirements/constraints.txt +0 -0
  45. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/setup.cfg +0 -0
  46. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/setup.py +0 -0
  47. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/tests/test_keys.py +0 -0
  48. {learning_paths_plugin-0.3.0 → learning_paths_plugin-0.3.1}/tests/test_receivers.py +0 -0
@@ -16,6 +16,24 @@ Unreleased
16
16
 
17
17
  *
18
18
 
19
+ 0.3.1 - 2025-04-14
20
+ ******************
21
+
22
+ Added
23
+ =====
24
+
25
+ * API for listing and retrieving Learning Paths.
26
+
27
+ Fixed
28
+ =====
29
+
30
+ * Automatically create grading criteria for Learning Paths.
31
+
32
+ Changed
33
+ =======
34
+
35
+ * Replaced relative due dates with actual due dates from course runs.
36
+
19
37
  0.3.0 - 2025-04-03
20
38
  ******************
21
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-paths-plugin
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Learning Paths plugin
5
5
  Home-page: https://github.com/open-craft/learning-paths-plugin
6
6
  Author: OpenCraft
@@ -120,6 +120,24 @@ Unreleased
120
120
 
121
121
  *
122
122
 
123
+ 0.3.1 - 2025-04-14
124
+ ******************
125
+
126
+ Added
127
+ =====
128
+
129
+ * API for listing and retrieving Learning Paths.
130
+
131
+ Fixed
132
+ =====
133
+
134
+ * Automatically create grading criteria for Learning Paths.
135
+
136
+ Changed
137
+ =======
138
+
139
+ * Replaced relative due dates with actual due dates from course runs.
140
+
123
141
  0.3.0 - 2025-04-03
124
142
  ******************
125
143
 
@@ -2,4 +2,4 @@
2
2
  Learning Paths plugin.
3
3
  """
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.3.1"
@@ -4,7 +4,14 @@ Serializer for LearningPath.
4
4
 
5
5
  from rest_framework import serializers
6
6
 
7
- from learning_paths.models import LearningPath, LearningPathEnrollment
7
+ from learning_paths.models import (
8
+ AcquiredSkill,
9
+ LearningPath,
10
+ LearningPathEnrollment,
11
+ LearningPathStep,
12
+ RequiredSkill,
13
+ Skill,
14
+ )
8
15
 
9
16
  DEFAULT_STATUS = "active"
10
17
  IMAGE_WIDTH = 1440
@@ -87,6 +94,98 @@ class LearningPathGradeSerializer(serializers.Serializer):
87
94
  required_grade = serializers.FloatField()
88
95
 
89
96
 
97
+ class LearningPathStepSerializer(serializers.ModelSerializer):
98
+ class Meta:
99
+ model = LearningPathStep
100
+ fields = ["order", "course_key", "due_date", "weight"]
101
+
102
+
103
+ class LearningPathListSerializer(serializers.ModelSerializer):
104
+ """Serializer for the learning path list."""
105
+
106
+ steps = LearningPathStepSerializer(many=True, read_only=True)
107
+ required_completion = serializers.FloatField(
108
+ source="grading_criteria.required_completion", read_only=True
109
+ )
110
+
111
+ class Meta:
112
+ model = LearningPath
113
+ fields = [
114
+ "key",
115
+ "slug",
116
+ "display_name",
117
+ "image_url",
118
+ "sequential",
119
+ "steps",
120
+ "required_completion",
121
+ ]
122
+
123
+
124
+ class SkillSerializer(serializers.ModelSerializer):
125
+ class Meta:
126
+ model = Skill
127
+ fields = ["id", "display_name"]
128
+
129
+
130
+ class RequiredSkillSerializer(serializers.ModelSerializer):
131
+ """
132
+ Serializer for required skill.
133
+ """
134
+
135
+ skill = SkillSerializer()
136
+
137
+ class Meta:
138
+ model = RequiredSkill
139
+ fields = ["skill", "level"]
140
+
141
+
142
+ class AcquiredSkillSerializer(serializers.ModelSerializer):
143
+ """
144
+ Serializer for acquired skill.
145
+ """
146
+
147
+ skill = SkillSerializer()
148
+
149
+ class Meta:
150
+ model = AcquiredSkill
151
+ fields = ["skill", "level"]
152
+
153
+
154
+ class LearningPathDetailSerializer(serializers.ModelSerializer):
155
+ """
156
+ Serializer for learning path details.
157
+ """
158
+
159
+ steps = LearningPathStepSerializer(many=True, read_only=True)
160
+ required_skills = RequiredSkillSerializer(
161
+ source="requiredskill_set", many=True, read_only=True
162
+ )
163
+ acquired_skills = AcquiredSkillSerializer(
164
+ source="acquiredskill_set", many=True, read_only=True
165
+ )
166
+ required_completion = serializers.FloatField(
167
+ source="grading_criteria.required_completion", read_only=True
168
+ )
169
+
170
+ class Meta:
171
+ model = LearningPath
172
+ fields = [
173
+ "key",
174
+ "slug",
175
+ "display_name",
176
+ "subtitle",
177
+ "description",
178
+ "image_url",
179
+ "level",
180
+ "duration_in_days",
181
+ "sequential",
182
+ "steps",
183
+ "required_skills",
184
+ "acquired_skills",
185
+ "required_completion",
186
+ ]
187
+
188
+
90
189
  class LearningPathEnrollmentSerializer(serializers.ModelSerializer):
91
190
  class Meta:
92
191
  model = LearningPathEnrollment
@@ -9,6 +9,7 @@ from learning_paths.api.v1.views import (
9
9
  LearningPathEnrollmentView,
10
10
  LearningPathUserGradeView,
11
11
  LearningPathUserProgressView,
12
+ LearningPathViewSet,
12
13
  ListEnrollmentsView,
13
14
  )
14
15
  from learning_paths.keys import LEARNING_PATH_URL_PATTERN
@@ -17,6 +18,7 @@ router = routers.SimpleRouter()
17
18
  router.register(
18
19
  r"programs", LearningPathAsProgramViewSet, basename="learning-path-as-program"
19
20
  )
21
+ router.register(r"learning-paths", LearningPathViewSet, basename="learning-path")
20
22
 
21
23
  urlpatterns = router.urls + [
22
24
  re_path(
@@ -12,6 +12,7 @@ from django.core.validators import validate_email
12
12
  from django.shortcuts import get_object_or_404
13
13
  from opaque_keys import InvalidKeyError
14
14
  from rest_framework import generics, status, viewsets
15
+ from rest_framework.exceptions import NotFound
15
16
  from rest_framework.pagination import PageNumberPagination
16
17
  from rest_framework.permissions import IsAdminUser, IsAuthenticated
17
18
  from rest_framework.response import Response
@@ -19,8 +20,10 @@ from rest_framework.views import APIView
19
20
 
20
21
  from learning_paths.api.v1.serializers import (
21
22
  LearningPathAsProgramSerializer,
23
+ LearningPathDetailSerializer,
22
24
  LearningPathEnrollmentSerializer,
23
25
  LearningPathGradeSerializer,
26
+ LearningPathListSerializer,
24
27
  LearningPathProgressSerializer,
25
28
  )
26
29
  from learning_paths.keys import LearningPathKey
@@ -125,6 +128,30 @@ class LearningPathUserGradeView(APIView):
125
128
  return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
126
129
 
127
130
 
131
+ class LearningPathViewSet(viewsets.ReadOnlyModelViewSet):
132
+ """
133
+ ViewSet for listing all learning paths and retrieving a specific learning path's details,
134
+ including steps and associated skills.
135
+ """
136
+
137
+ queryset = LearningPath.objects.prefetch_related("steps", "grading_criteria")
138
+ permission_classes = (IsAuthenticated,)
139
+ pagination_class = PageNumberPagination
140
+ lookup_field = "key"
141
+
142
+ def get_serializer_class(self):
143
+ if self.action == "list":
144
+ return LearningPathListSerializer
145
+ return LearningPathDetailSerializer
146
+
147
+ def get_object(self):
148
+ """Gracefully handle an invalid learning path key format."""
149
+ try:
150
+ return super().get_object()
151
+ except InvalidKeyError as exc:
152
+ raise NotFound("Invalid learning path key format.") from exc
153
+
154
+
128
155
  class LearningPathEnrollmentView(APIView):
129
156
  """
130
157
  API View to handle changes to LearningPathEnrollment model
@@ -2,6 +2,8 @@
2
2
  Compatibility layer for testing without Open edX.
3
3
  """
4
4
 
5
+ from datetime import datetime
6
+
5
7
  from django.contrib.auth.models import AbstractBaseUser
6
8
  from opaque_keys.edx.keys import CourseKey
7
9
 
@@ -41,8 +43,23 @@ def get_course_keys_with_outlines():
41
43
  return course_keys_with_outlines()
42
44
 
43
45
 
46
+ def get_course_due_date(course_key: CourseKey) -> datetime | None:
47
+ """
48
+ Retrieve course end date.
49
+ """
50
+ # pylint: disable=import-outside-toplevel, import-error
51
+ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
52
+
53
+ try:
54
+ overview = CourseOverview.objects.get(id=course_key)
55
+ return overview.end
56
+ except CourseOverview.DoesNotExist:
57
+ return None
58
+
59
+
44
60
  __all__ = [
45
61
  "get_course_keys_with_outlines",
46
62
  "get_catalog_api_client",
47
63
  "get_user_course_grade",
64
+ "get_course_due_date",
48
65
  ]
@@ -0,0 +1,17 @@
1
+ # Generated by Django 4.2.16 on 2025-03-24 10:37
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('learning_paths', '0007_replace_uuid_with_learningpathkey'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='learningpathstep',
15
+ name='relative_due_date_in_days',
16
+ ),
17
+ ]
@@ -2,7 +2,7 @@
2
2
  Database models for learning_paths.
3
3
  """
4
4
 
5
- from datetime import timedelta
5
+ from datetime import datetime, timedelta
6
6
  from uuid import uuid4
7
7
 
8
8
  from django.contrib import auth
@@ -13,7 +13,7 @@ from model_utils.models import TimeStampedModel
13
13
  from opaque_keys.edx.django.models import CourseKeyField
14
14
  from simple_history.models import HistoricalRecords
15
15
 
16
- from .compat import get_user_course_grade
16
+ from .compat import get_course_due_date, get_user_course_grade
17
17
  from .keys import LearningPathKeyField
18
18
 
19
19
  User = auth.get_user_model()
@@ -88,6 +88,14 @@ class LearningPath(TimeStampedModel):
88
88
  """User-friendly string representation of this model."""
89
89
  return self.display_name
90
90
 
91
+ def save(self, *args, **kwargs):
92
+ """Create default grading criteria when a new learning path is created."""
93
+ is_new = self._state.adding
94
+ super().save(*args, **kwargs)
95
+
96
+ if is_new and not hasattr(self, "grading_criteria"):
97
+ LearningPathGradingCriteria.objects.get_or_create(learning_path=self)
98
+
91
99
 
92
100
  class LearningPathStep(TimeStampedModel):
93
101
  """
@@ -105,14 +113,6 @@ class LearningPathStep(TimeStampedModel):
105
113
  learning_path = models.ForeignKey(
106
114
  LearningPath, related_name="steps", on_delete=models.CASCADE
107
115
  )
108
- relative_due_date_in_days = models.PositiveIntegerField(
109
- blank=True,
110
- null=True,
111
- verbose_name=_("Due date (days)"),
112
- help_text=_(
113
- "Used to calculate the due date from the starting date of the course."
114
- ),
115
- )
116
116
  order = models.PositiveIntegerField(
117
117
  blank=True,
118
118
  null=True,
@@ -130,6 +130,11 @@ class LearningPathStep(TimeStampedModel):
130
130
  ),
131
131
  )
132
132
 
133
+ @property
134
+ def due_date(self) -> datetime | None:
135
+ """Retrieve the due date for this course."""
136
+ return get_course_due_date(self.course_key)
137
+
133
138
  def __str__(self):
134
139
  """User-friendly string representation of this model."""
135
140
  return "{}: {}".format(self.order, self.course_key)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-paths-plugin
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Learning Paths plugin
5
5
  Home-page: https://github.com/open-craft/learning-paths-plugin
6
6
  Author: OpenCraft
@@ -120,6 +120,24 @@ Unreleased
120
120
 
121
121
  *
122
122
 
123
+ 0.3.1 - 2025-04-14
124
+ ******************
125
+
126
+ Added
127
+ =====
128
+
129
+ * API for listing and retrieving Learning Paths.
130
+
131
+ Fixed
132
+ =====
133
+
134
+ * Automatically create grading criteria for Learning Paths.
135
+
136
+ Changed
137
+ =======
138
+
139
+ * Replaced relative due dates with actual due dates from course runs.
140
+
123
141
  0.3.0 - 2025-04-03
124
142
  ******************
125
143
 
@@ -30,6 +30,7 @@ learning_paths/migrations/0004_auto_20240207_1633.py
30
30
  learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py
31
31
  learning_paths/migrations/0006_enrollment_models.py
32
32
  learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py
33
+ learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py
33
34
  learning_paths/migrations/__init__.py
34
35
  learning_paths/templates/learning_paths/base.html
35
36
  learning_paths_plugin.egg-info/PKG-INFO
@@ -77,3 +77,11 @@ class TestLearningPath:
77
77
  key=LearningPathKey("org2", "number2", "run2", "group2"),
78
78
  slug=learning_path.slug,
79
79
  )
80
+
81
+ def test_grading_criteria_auto_creation(self, learning_path):
82
+ """Test that grading criteria is automatically created with a learning path."""
83
+
84
+ criteria = learning_path.grading_criteria
85
+ assert criteria is not None
86
+ assert criteria.required_completion == 0.80
87
+ assert criteria.required_grade == 0.75