learning-paths-plugin 0.3.3__tar.gz → 0.3.4__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 (57) hide show
  1. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/CHANGELOG.rst +19 -0
  2. {learning_paths_plugin-0.3.3/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4}/PKG-INFO +21 -2
  3. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/__init__.py +1 -1
  4. learning_paths_plugin-0.3.4/learning_paths/admin.py +434 -0
  5. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/serializers.py +10 -9
  6. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/views.py +151 -33
  7. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/compat.py +4 -6
  8. learning_paths_plugin-0.3.4/learning_paths/conftest.py +51 -0
  9. learning_paths_plugin-0.3.4/learning_paths/migrations/0006_enrollment_models.py +39 -0
  10. learning_paths_plugin-0.3.4/learning_paths/migrations/0013_enrollment_audit.py +169 -0
  11. learning_paths_plugin-0.3.4/learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py +59 -0
  12. learning_paths_plugin-0.3.4/learning_paths/migrations/0015_make_skill_level_optional.py +27 -0
  13. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/models.py +105 -39
  14. learning_paths_plugin-0.3.4/learning_paths/receivers.py +139 -0
  15. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4/learning_paths_plugin.egg-info}/PKG-INFO +21 -2
  16. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/SOURCES.txt +3 -0
  17. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/requires.txt +1 -1
  18. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/requirements/base.in +1 -1
  19. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/requirements/constraints.txt +1 -6
  20. learning_paths_plugin-0.3.3/learning_paths/admin.py +0 -202
  21. learning_paths_plugin-0.3.3/learning_paths/conftest.py +0 -13
  22. learning_paths_plugin-0.3.3/learning_paths/migrations/0006_enrollment_models.py +0 -66
  23. learning_paths_plugin-0.3.3/learning_paths/receivers.py +0 -46
  24. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/LICENSE.txt +0 -0
  25. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/MANIFEST.in +0 -0
  26. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/README.rst +0 -0
  27. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/__init__.py +0 -0
  28. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/urls.py +0 -0
  29. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/__init__.py +0 -0
  30. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/filters.py +0 -0
  31. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/permissions.py +0 -0
  32. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/urls.py +0 -0
  33. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/api/v1/utils.py +0 -0
  34. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/apps.py +0 -0
  35. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/keys.py +0 -0
  36. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0001_initial.py +0 -0
  37. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
  38. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
  39. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
  40. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
  41. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
  42. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +0 -0
  43. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
  44. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
  45. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
  46. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
  47. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/migrations/__init__.py +0 -0
  48. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/settings.py +0 -0
  49. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/templates/learning_paths/base.html +0 -0
  50. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths/urls.py +0 -0
  51. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
  52. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
  53. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
  54. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/learning_paths_plugin.egg-info/top_level.txt +0 -0
  55. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/pyproject.toml +0 -0
  56. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/setup.cfg +0 -0
  57. {learning_paths_plugin-0.3.3 → learning_paths_plugin-0.3.4}/setup.py +0 -0
@@ -16,6 +16,25 @@ Unreleased
16
16
 
17
17
  *
18
18
 
19
+ 0.3.4 - 2025-08-02
20
+ ******************
21
+
22
+ Added
23
+ =====
24
+
25
+ * Bulk unenrollment API.
26
+ * Enrollment audit model that tracks the enrollment state transitions.
27
+ * Allow specifying time commitment.
28
+ * Allow duplicating Learning Paths in the Django admin interface.
29
+
30
+ Changed
31
+ =======
32
+
33
+ * The Learning Paths API includes start and end dates for its steps.
34
+ * Return enrollment date in the API instead of a boolean.
35
+ * Allow specifying any text for the duration.
36
+ * Make the skill level optional.
37
+
19
38
  0.3.3 - 2025-05-23
20
39
  ******************
21
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-paths-plugin
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Learning Paths plugin
5
5
  Home-page: https://github.com/open-craft/learning-paths-plugin
6
6
  Author: OpenCraft
@@ -18,7 +18,7 @@ Requires-Python: >=3.11
18
18
  License-File: LICENSE.txt
19
19
  Requires-Dist: Django
20
20
  Requires-Dist: django-model-utils
21
- Requires-Dist: django-simple-history==3.4.0
21
+ Requires-Dist: django-object-actions
22
22
  Requires-Dist: djangorestframework
23
23
  Requires-Dist: edx-django-utils
24
24
  Requires-Dist: edx-opaque-keys
@@ -121,6 +121,25 @@ Unreleased
121
121
 
122
122
  *
123
123
 
124
+ 0.3.4 - 2025-08-02
125
+ ******************
126
+
127
+ Added
128
+ =====
129
+
130
+ * Bulk unenrollment API.
131
+ * Enrollment audit model that tracks the enrollment state transitions.
132
+ * Allow specifying time commitment.
133
+ * Allow duplicating Learning Paths in the Django admin interface.
134
+
135
+ Changed
136
+ =======
137
+
138
+ * The Learning Paths API includes start and end dates for its steps.
139
+ * Return enrollment date in the API instead of a boolean.
140
+ * Allow specifying any text for the duration.
141
+ * Make the skill level optional.
142
+
124
143
  0.3.3 - 2025-05-23
125
144
  ******************
126
145
 
@@ -2,4 +2,4 @@
2
2
  Learning Paths plugin.
3
3
  """
4
4
 
5
- __version__ = "0.3.3"
5
+ __version__ = "0.3.4"
@@ -0,0 +1,434 @@
1
+ """
2
+ Django Admin for learning_paths.
3
+ """
4
+
5
+ import os
6
+
7
+ from django import forms
8
+ from django.contrib import admin, auth, messages
9
+ from django.core.exceptions import ValidationError
10
+ from django.core.files.base import ContentFile
11
+ from django.db import transaction
12
+ from django.http import HttpResponseRedirect
13
+ from django.urls import reverse
14
+ from django.utils.translation import gettext_lazy as _
15
+ from django_object_actions import DjangoObjectActions, action
16
+
17
+ from .compat import get_course_keys_with_outlines
18
+ from .models import (
19
+ AcquiredSkill,
20
+ LearningPath,
21
+ LearningPathEnrollment,
22
+ LearningPathEnrollmentAllowed,
23
+ LearningPathEnrollmentAudit,
24
+ LearningPathGradingCriteria,
25
+ LearningPathStep,
26
+ RequiredSkill,
27
+ Skill,
28
+ )
29
+
30
+ User = auth.get_user_model()
31
+
32
+
33
+ def get_course_keys_choices():
34
+ """Get course keys in an adequate format for a choice field."""
35
+ yield None, ""
36
+ for key in get_course_keys_with_outlines():
37
+ yield key, key
38
+
39
+
40
+ class CourseKeyDatalistWidget(forms.TextInput):
41
+ """A widget that provides a datalist for course keys."""
42
+
43
+ def __init__(self, choices=None, attrs=None):
44
+ """Initialize the widget with a datalist and apply styles."""
45
+ attrs = attrs or {}
46
+ attrs.update(
47
+ {
48
+ "style": "width: 30em;",
49
+ "class": "form-control datalist-input",
50
+ "placeholder": _("Type to search courses..."),
51
+ }
52
+ )
53
+ super().__init__(attrs)
54
+ self.choices = choices or []
55
+
56
+ def render(self, name, value, attrs=None, renderer=None):
57
+ """Render the widget with a datalist."""
58
+ final_attrs = attrs or {}
59
+ data_list_id = f"datalist_{name}"
60
+ final_attrs["list"] = data_list_id
61
+
62
+ text_input_html = super().render(name, value, attrs, renderer)
63
+ data_list_id = f"datalist_{name}"
64
+ options = "\n".join(f'<option value="{choice}" />' for choice in self.choices)
65
+ datalist_html = f'<datalist id="{data_list_id}">\n{options}\n</datalist>'
66
+ return f"{text_input_html}\n{datalist_html}"
67
+
68
+
69
+ class LearningPathStepForm(forms.ModelForm):
70
+ """Form for Learning Path step."""
71
+
72
+ def __init__(self, *args, **kwargs):
73
+ """Lazily fetch course keys to avoid calling compat code in all environments."""
74
+ super().__init__(*args, **kwargs)
75
+ self._course_keys = get_course_keys_with_outlines()
76
+ self.fields["course_key"].widget = CourseKeyDatalistWidget(choices=self._course_keys)
77
+
78
+ course_key = forms.CharField(label=_("Course"))
79
+
80
+ def clean_course_key(self):
81
+ """Validate that the course key is on the list of available course keys."""
82
+ course_key = self.cleaned_data.get("course_key")
83
+ valid_keys = {str(key).strip() for key in self._course_keys}
84
+
85
+ if course_key not in valid_keys:
86
+ raise ValidationError(_("Invalid course key. Please select a course from the suggestions."))
87
+
88
+ return course_key
89
+
90
+
91
+ class LearningPathStepInline(admin.TabularInline):
92
+ """Inline Admin for Learning Path step."""
93
+
94
+ model = LearningPathStep
95
+ form = LearningPathStepForm
96
+
97
+
98
+ class AcquiredSkillInline(admin.TabularInline):
99
+ """Inline Admin for Learning Path acquired skill."""
100
+
101
+ model = AcquiredSkill
102
+
103
+
104
+ class RequiredSkillInline(admin.TabularInline):
105
+ """Inline Admin for Learning Path required skill."""
106
+
107
+ model = RequiredSkill
108
+
109
+
110
+ class LearningPathGradingCriteriaInline(admin.TabularInline):
111
+ """Inline Admin for Learning path grading criteria."""
112
+
113
+ model = LearningPathGradingCriteria
114
+ verbose_name = "Certificate Criteria"
115
+
116
+
117
+ class BulkEnrollUsersForm(forms.ModelForm):
118
+ """Form to bulk enroll users in a learning path."""
119
+
120
+ usernames = forms.CharField(
121
+ widget=forms.Textarea,
122
+ help_text="Enter usernames separated by newlines",
123
+ label="Bulk enroll users",
124
+ required=False,
125
+ )
126
+
127
+ class Meta:
128
+ """Form options."""
129
+
130
+ model = LearningPath
131
+ fields = "__all__"
132
+
133
+ def clean_usernames(self):
134
+ """Validate usernames and return a list of users."""
135
+ data = self.cleaned_data["usernames"]
136
+ if not data:
137
+ return []
138
+ usernames = [username.strip() for username in data.split("\n")]
139
+ users = User.objects.filter(username__in=usernames)
140
+ found_usernames = list(users.values_list("username", flat=True))
141
+ invalid_usernames = set(usernames) - set(found_usernames)
142
+ if invalid_usernames:
143
+ raise ValidationError(f"The following usernames are not valid: {', '.join(invalid_usernames)}")
144
+ return users
145
+
146
+
147
+ @admin.register(LearningPath)
148
+ class LearningPathAdmin(DjangoObjectActions, admin.ModelAdmin):
149
+ """Admin for Learning Path."""
150
+
151
+ model = LearningPath
152
+ form = BulkEnrollUsersForm
153
+
154
+ search_fields = [
155
+ "display_name",
156
+ "key",
157
+ ]
158
+ list_display = (
159
+ "key",
160
+ "display_name",
161
+ "level",
162
+ "duration",
163
+ "invite_only",
164
+ )
165
+ list_filter = ("invite_only",)
166
+ readonly_fields = ("key",)
167
+
168
+ inlines = [
169
+ LearningPathStepInline,
170
+ RequiredSkillInline,
171
+ AcquiredSkillInline,
172
+ LearningPathGradingCriteriaInline,
173
+ ]
174
+
175
+ change_actions = ("duplicate_learning_path",)
176
+
177
+ def get_readonly_fields(self, request, obj=None):
178
+ """Make key read-only only for existing objects."""
179
+ if obj: # Editing an existing object.
180
+ return self.readonly_fields
181
+ return () # Allow all fields during creation.
182
+
183
+ def save_related(self, request, form, formsets, change):
184
+ """Save related objects and enroll users in the learning path."""
185
+ super().save_related(request, form, formsets, change)
186
+ with transaction.atomic():
187
+ for user in form.cleaned_data["usernames"]:
188
+ LearningPathEnrollment.objects.get_or_create(user=user, learning_path=form.instance)
189
+
190
+ @action(label="Duplicate Learning Path", description="Create a copy of this Learning Path")
191
+ def duplicate_learning_path(self, request, obj: LearningPath) -> HttpResponseRedirect:
192
+ """Duplicate the learning path with a new unique key."""
193
+ base_new_key = f"{str(obj.key)}_copy"
194
+ new_key = base_new_key
195
+ counter = 1
196
+
197
+ while LearningPath.objects.filter(key=new_key).exists():
198
+ new_key = f"{base_new_key}_{counter}"
199
+ counter += 1
200
+
201
+ with transaction.atomic():
202
+ new_learning_path = LearningPath(
203
+ key=new_key,
204
+ display_name=f"{obj.display_name} (Copy)",
205
+ subtitle=obj.subtitle,
206
+ description=obj.description,
207
+ level=obj.level,
208
+ duration=obj.duration,
209
+ time_commitment=obj.time_commitment,
210
+ sequential=obj.sequential,
211
+ invite_only=obj.invite_only,
212
+ )
213
+
214
+ if obj.image:
215
+ with obj.image.open("rb") as original_file:
216
+ image_content = original_file.read()
217
+
218
+ original_filename = os.path.basename(obj.image.name)
219
+ new_learning_path.image.save(original_filename, ContentFile(image_content), save=False)
220
+
221
+ new_learning_path.save()
222
+
223
+ new_learning_path.refresh_from_db()
224
+ new_learning_path.grading_criteria.required_completion = obj.grading_criteria.required_completion
225
+ new_learning_path.grading_criteria.required_grade = obj.grading_criteria.required_grade
226
+ new_learning_path.grading_criteria.save()
227
+
228
+ for step in obj.steps.all():
229
+ step.pk = None
230
+ step.learning_path = new_learning_path
231
+ step.save()
232
+
233
+ for skill in obj.requiredskill_set.all():
234
+ skill.pk = None
235
+ skill.learning_path = new_learning_path
236
+ skill.save()
237
+
238
+ for skill in obj.acquiredskill_set.all():
239
+ skill.pk = None
240
+ skill.learning_path = new_learning_path
241
+ skill.save()
242
+
243
+ messages.success(request, f"Learning path duplicated successfully. New key: {new_key}")
244
+ return HttpResponseRedirect(reverse("admin:learning_paths_learningpath_change", args=[new_learning_path.pk]))
245
+
246
+
247
+ @admin.register(Skill)
248
+ class SkillAdmin(admin.ModelAdmin):
249
+ """Admin for Learning Path generic skill."""
250
+
251
+ model = Skill
252
+
253
+
254
+ class EnrollmentAuditInline(admin.TabularInline):
255
+ """Inline admin for LearningPathEnrollmentAudit records."""
256
+
257
+ model = LearningPathEnrollmentAudit
258
+ fk_name = "enrollment"
259
+ extra = 0
260
+ exclude = ["enrollment_allowed"]
261
+ readonly_fields = [
262
+ "state_transition",
263
+ "enrolled_by",
264
+ "reason",
265
+ "org",
266
+ "role",
267
+ "created",
268
+ ]
269
+
270
+ def has_add_permission(self, request, obj=None):
271
+ """Disable manual creation of audit records."""
272
+ return False
273
+
274
+ def has_delete_permission(self, request, obj=None):
275
+ """Disable deletion of audit records."""
276
+ return False
277
+
278
+
279
+ class EnrollmentAllowedAuditInline(admin.TabularInline):
280
+ """Inline admin for LearningPathEnrollmentAudit records related to enrollment allowed."""
281
+
282
+ model = LearningPathEnrollmentAudit
283
+ fk_name = "enrollment_allowed"
284
+ extra = 0
285
+ exclude = ["enrollment"]
286
+ readonly_fields = [
287
+ "state_transition",
288
+ "enrolled_by",
289
+ "reason",
290
+ "org",
291
+ "role",
292
+ "created",
293
+ ]
294
+
295
+ def has_add_permission(self, request, obj=None):
296
+ """Disable manual creation of audit records."""
297
+ return False
298
+
299
+ def has_delete_permission(self, request, obj=None):
300
+ """Disable deletion of audit records."""
301
+ return False
302
+
303
+
304
+ @admin.register(LearningPathEnrollment)
305
+ class EnrolledUsersAdmin(admin.ModelAdmin):
306
+ """Admin for Learning Path enrollment."""
307
+
308
+ model = LearningPathEnrollment
309
+ raw_id_fields = ("user",)
310
+ autocomplete_fields = ["learning_path"]
311
+ inlines = [EnrollmentAuditInline]
312
+
313
+ list_display = [
314
+ "id",
315
+ "user",
316
+ "learning_path",
317
+ "is_active",
318
+ "created",
319
+ ]
320
+
321
+ list_filter = [
322
+ "learning_path__key",
323
+ "created",
324
+ "is_active",
325
+ ]
326
+
327
+ search_fields = [
328
+ "id",
329
+ "user__username",
330
+ "learning_path__key",
331
+ "learning_path__display_name",
332
+ ]
333
+
334
+
335
+ @admin.register(LearningPathEnrollmentAllowed)
336
+ class EnrollmentAllowedAdmin(admin.ModelAdmin):
337
+ """Admin configuration for LearningPathEnrollmentAllowed model."""
338
+
339
+ list_display = [
340
+ "id",
341
+ "email",
342
+ "get_user",
343
+ "learning_path",
344
+ "created",
345
+ ]
346
+
347
+ list_filter = [
348
+ "learning_path",
349
+ "created",
350
+ ]
351
+
352
+ search_fields = [
353
+ "email",
354
+ "user__username",
355
+ "user__email",
356
+ "learning_path__key",
357
+ ]
358
+
359
+ readonly_fields = [
360
+ "user",
361
+ "created",
362
+ "modified",
363
+ ]
364
+
365
+ inlines = [EnrollmentAllowedAuditInline]
366
+
367
+ def get_user(self, obj):
368
+ """Get the associated user, if any."""
369
+ return obj.user.username if obj.user else "-"
370
+
371
+ get_user.short_description = "User"
372
+
373
+
374
+ @admin.register(LearningPathEnrollmentAudit)
375
+ class EnrollmentAuditAdmin(admin.ModelAdmin):
376
+ """Admin configuration for LearningPathEnrollmentAudit model."""
377
+
378
+ list_display = [
379
+ "id",
380
+ "state_transition",
381
+ "enrolled_by",
382
+ "get_enrollee",
383
+ "get_learning_path",
384
+ "created",
385
+ "org",
386
+ "role",
387
+ ]
388
+
389
+ list_filter = [
390
+ "state_transition",
391
+ "created",
392
+ "org",
393
+ "role",
394
+ ]
395
+
396
+ search_fields = [
397
+ "enrolled_by__username",
398
+ "enrolled_by__email",
399
+ "enrollment__user__username",
400
+ "enrollment__user__email",
401
+ "enrollment_allowed__email",
402
+ "enrollment__learning_path__key",
403
+ "enrollment_allowed__learning_path__key",
404
+ "reason",
405
+ ]
406
+
407
+ readonly_fields = [
408
+ "enrollment",
409
+ "enrollment_allowed",
410
+ "enrolled_by",
411
+ "state_transition",
412
+ "created",
413
+ "modified",
414
+ ]
415
+
416
+ def get_enrollee(self, obj):
417
+ """Get the enrollee (user or email)."""
418
+ if obj.enrollment:
419
+ return obj.enrollment.user.username
420
+ elif obj.enrollment_allowed:
421
+ return obj.enrollment_allowed.user.username if obj.enrollment_allowed.user else obj.enrollment_allowed.email
422
+ return "-"
423
+
424
+ get_enrollee.short_description = "Enrollee"
425
+
426
+ def get_learning_path(self, obj):
427
+ """Get the learning path title."""
428
+ if obj.enrollment:
429
+ return obj.enrollment.learning_path.key
430
+ elif obj.enrollment_allowed:
431
+ return obj.enrollment_allowed.learning_path.key
432
+ return "-"
433
+
434
+ get_learning_path.short_description = "Learning Path"
@@ -97,7 +97,7 @@ class LearningPathGradeSerializer(serializers.Serializer):
97
97
  class LearningPathStepSerializer(serializers.ModelSerializer):
98
98
  class Meta:
99
99
  model = LearningPathStep
100
- fields = ["order", "course_key", "due_date", "weight"]
100
+ fields = ["order", "course_key", "course_dates", "weight"]
101
101
 
102
102
 
103
103
  class LearningPathListSerializer(serializers.ModelSerializer):
@@ -105,7 +105,7 @@ class LearningPathListSerializer(serializers.ModelSerializer):
105
105
 
106
106
  steps = LearningPathStepSerializer(many=True, read_only=True)
107
107
  required_completion = serializers.FloatField(source="grading_criteria.required_completion", read_only=True)
108
- is_enrolled = serializers.SerializerMethodField()
108
+ enrollment_date = serializers.SerializerMethodField()
109
109
  invite_only = serializers.BooleanField()
110
110
  image = serializers.ImageField(read_only=True)
111
111
 
@@ -118,17 +118,17 @@ class LearningPathListSerializer(serializers.ModelSerializer):
118
118
  "sequential",
119
119
  "steps",
120
120
  "required_completion",
121
- "is_enrolled",
121
+ "enrollment_date",
122
122
  "invite_only",
123
123
  ]
124
124
 
125
- def get_is_enrolled(self, obj):
125
+ def get_enrollment_date(self, obj):
126
126
  """
127
127
  Check if the current user is enrolled in this learning path.
128
128
  """
129
- if hasattr(obj, "is_enrolled"):
130
- return obj.is_enrolled
131
- return False
129
+ if hasattr(obj, "enrollment_date"):
130
+ return obj.enrollment_date
131
+ return None
132
132
 
133
133
 
134
134
  class SkillSerializer(serializers.ModelSerializer):
@@ -174,7 +174,8 @@ class LearningPathDetailSerializer(LearningPathListSerializer):
174
174
  "subtitle",
175
175
  "description",
176
176
  "level",
177
- "duration_in_days",
177
+ "duration",
178
+ "time_commitment",
178
179
  "required_skills",
179
180
  "acquired_skills",
180
181
  ]
@@ -183,4 +184,4 @@ class LearningPathDetailSerializer(LearningPathListSerializer):
183
184
  class LearningPathEnrollmentSerializer(serializers.ModelSerializer):
184
185
  class Meta:
185
186
  model = LearningPathEnrollment
186
- fields = ("user", "learning_path", "is_active", "enrolled_at")
187
+ fields = ("user", "learning_path", "is_active", "created")