learning-paths-plugin 0.3.4__tar.gz → 0.3.4rc8__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.
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/CHANGELOG.rst +1 -5
- {learning_paths_plugin-0.3.4/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc8}/PKG-INFO +2 -7
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/__init__.py +1 -1
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/admin.py +3 -68
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/serializers.py +1 -2
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/views.py +1 -2
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/models.py +43 -27
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8/learning_paths_plugin.egg-info}/PKG-INFO +2 -7
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/SOURCES.txt +0 -2
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/requires.txt +0 -1
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/requirements/base.in +0 -1
- learning_paths_plugin-0.3.4/learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py +0 -59
- learning_paths_plugin-0.3.4/learning_paths/migrations/0015_make_skill_level_optional.py +0 -27
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/LICENSE.txt +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/MANIFEST.in +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/README.rst +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/__init__.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/urls.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/__init__.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/filters.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/permissions.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/urls.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/utils.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/apps.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/compat.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/conftest.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/keys.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0001_initial.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0006_enrollment_models.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0013_enrollment_audit.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/__init__.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/receivers.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/settings.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/templates/learning_paths/base.html +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/urls.py +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/top_level.txt +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/pyproject.toml +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/requirements/constraints.txt +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/setup.cfg +0 -0
- {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/setup.py +0 -0
|
@@ -16,7 +16,7 @@ Unreleased
|
|
|
16
16
|
|
|
17
17
|
*
|
|
18
18
|
|
|
19
|
-
0.3.4 - 2025-
|
|
19
|
+
0.3.4 - 2025-05-23
|
|
20
20
|
******************
|
|
21
21
|
|
|
22
22
|
Added
|
|
@@ -24,16 +24,12 @@ Added
|
|
|
24
24
|
|
|
25
25
|
* Bulk unenrollment API.
|
|
26
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
27
|
|
|
30
28
|
Changed
|
|
31
29
|
=======
|
|
32
30
|
|
|
33
31
|
* The Learning Paths API includes start and end dates for its steps.
|
|
34
32
|
* 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
33
|
|
|
38
34
|
0.3.3 - 2025-05-23
|
|
39
35
|
******************
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-paths-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4rc8
|
|
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,6 @@ 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-object-actions
|
|
22
21
|
Requires-Dist: djangorestframework
|
|
23
22
|
Requires-Dist: edx-django-utils
|
|
24
23
|
Requires-Dist: edx-opaque-keys
|
|
@@ -121,7 +120,7 @@ Unreleased
|
|
|
121
120
|
|
|
122
121
|
*
|
|
123
122
|
|
|
124
|
-
0.3.4 - 2025-
|
|
123
|
+
0.3.4 - 2025-05-23
|
|
125
124
|
******************
|
|
126
125
|
|
|
127
126
|
Added
|
|
@@ -129,16 +128,12 @@ Added
|
|
|
129
128
|
|
|
130
129
|
* Bulk unenrollment API.
|
|
131
130
|
* 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
131
|
|
|
135
132
|
Changed
|
|
136
133
|
=======
|
|
137
134
|
|
|
138
135
|
* The Learning Paths API includes start and end dates for its steps.
|
|
139
136
|
* 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
137
|
|
|
143
138
|
0.3.3 - 2025-05-23
|
|
144
139
|
******************
|
|
@@ -2,17 +2,11 @@
|
|
|
2
2
|
Django Admin for learning_paths.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import os
|
|
6
|
-
|
|
7
5
|
from django import forms
|
|
8
|
-
from django.contrib import admin, auth
|
|
6
|
+
from django.contrib import admin, auth
|
|
9
7
|
from django.core.exceptions import ValidationError
|
|
10
|
-
from django.core.files.base import ContentFile
|
|
11
8
|
from django.db import transaction
|
|
12
|
-
from django.http import HttpResponseRedirect
|
|
13
|
-
from django.urls import reverse
|
|
14
9
|
from django.utils.translation import gettext_lazy as _
|
|
15
|
-
from django_object_actions import DjangoObjectActions, action
|
|
16
10
|
|
|
17
11
|
from .compat import get_course_keys_with_outlines
|
|
18
12
|
from .models import (
|
|
@@ -111,7 +105,6 @@ class LearningPathGradingCriteriaInline(admin.TabularInline):
|
|
|
111
105
|
"""Inline Admin for Learning path grading criteria."""
|
|
112
106
|
|
|
113
107
|
model = LearningPathGradingCriteria
|
|
114
|
-
verbose_name = "Certificate Criteria"
|
|
115
108
|
|
|
116
109
|
|
|
117
110
|
class BulkEnrollUsersForm(forms.ModelForm):
|
|
@@ -145,7 +138,7 @@ class BulkEnrollUsersForm(forms.ModelForm):
|
|
|
145
138
|
|
|
146
139
|
|
|
147
140
|
@admin.register(LearningPath)
|
|
148
|
-
class LearningPathAdmin(
|
|
141
|
+
class LearningPathAdmin(admin.ModelAdmin):
|
|
149
142
|
"""Admin for Learning Path."""
|
|
150
143
|
|
|
151
144
|
model = LearningPath
|
|
@@ -159,7 +152,7 @@ class LearningPathAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
|
159
152
|
"key",
|
|
160
153
|
"display_name",
|
|
161
154
|
"level",
|
|
162
|
-
"
|
|
155
|
+
"duration_in_days",
|
|
163
156
|
"invite_only",
|
|
164
157
|
)
|
|
165
158
|
list_filter = ("invite_only",)
|
|
@@ -172,8 +165,6 @@ class LearningPathAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
|
172
165
|
LearningPathGradingCriteriaInline,
|
|
173
166
|
]
|
|
174
167
|
|
|
175
|
-
change_actions = ("duplicate_learning_path",)
|
|
176
|
-
|
|
177
168
|
def get_readonly_fields(self, request, obj=None):
|
|
178
169
|
"""Make key read-only only for existing objects."""
|
|
179
170
|
if obj: # Editing an existing object.
|
|
@@ -187,62 +178,6 @@ class LearningPathAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
|
187
178
|
for user in form.cleaned_data["usernames"]:
|
|
188
179
|
LearningPathEnrollment.objects.get_or_create(user=user, learning_path=form.instance)
|
|
189
180
|
|
|
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
181
|
|
|
247
182
|
@admin.register(Skill)
|
|
248
183
|
class SkillAdmin(admin.ModelAdmin):
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/views.py
RENAMED
|
@@ -410,8 +410,7 @@ class BulkEnrollView(APIView):
|
|
|
410
410
|
allowed, created = LearningPathEnrollmentAllowed.objects.get_or_create(
|
|
411
411
|
email=email, learning_path=learning_path
|
|
412
412
|
)
|
|
413
|
-
if created
|
|
414
|
-
allowed.is_active = True
|
|
413
|
+
if created:
|
|
415
414
|
enrollment_allowed_created.append(allowed)
|
|
416
415
|
|
|
417
416
|
audit_data = self._create_audit_data(request, LearningPathEnrollmentAudit.UNENROLLED_TO_ALLOWEDTOENROLL)
|
|
@@ -5,7 +5,7 @@ Database models for learning_paths.
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import uuid
|
|
8
|
-
from datetime import datetime
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
from django.contrib import auth
|
|
@@ -53,11 +53,15 @@ class LearningPathManager(models.Manager):
|
|
|
53
53
|
enrollment_subquery = LearningPathEnrollment.objects.filter(
|
|
54
54
|
learning_path=OuterRef("pk"), user=user, is_active=True
|
|
55
55
|
).values("created")[:1]
|
|
56
|
-
queryset = queryset.annotate(
|
|
56
|
+
queryset = queryset.annotate(
|
|
57
|
+
enrollment_date=models.Subquery(enrollment_subquery)
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
# Apply visibility filtering based on the user role.
|
|
59
61
|
if not user.is_staff:
|
|
60
|
-
queryset = queryset.filter(
|
|
62
|
+
queryset = queryset.filter(
|
|
63
|
+
Q(invite_only=False) | Q(enrollment_date__isnull=False)
|
|
64
|
+
)
|
|
61
65
|
|
|
62
66
|
# Order by enrollment date (the most recent first), with null values at the end.
|
|
63
67
|
return queryset.order_by(models.F("enrollment_date").desc(nulls_last=True))
|
|
@@ -104,27 +108,27 @@ class LearningPath(TimeStampedModel):
|
|
|
104
108
|
subtitle = models.TextField(blank=True)
|
|
105
109
|
description = models.TextField(blank=True)
|
|
106
110
|
image = models.ImageField(
|
|
107
|
-
upload_to=_learning_path_image_upload_path,
|
|
111
|
+
upload_to=_learning_path_image_upload_path,
|
|
108
112
|
blank=True,
|
|
109
113
|
null=True,
|
|
110
114
|
verbose_name=_("Image"),
|
|
111
115
|
help_text=_("Image representing this Learning Path."),
|
|
112
116
|
)
|
|
113
117
|
level = models.CharField(max_length=255, blank=True, choices=LEVEL_CHOICES)
|
|
114
|
-
|
|
115
|
-
max_length=255,
|
|
116
|
-
blank=True,
|
|
117
|
-
help_text=_("Approximate time it should take to complete this Learning Path. Example: '10 Weeks'."),
|
|
118
|
-
)
|
|
119
|
-
time_commitment = models.CharField(
|
|
120
|
-
max_length=255,
|
|
118
|
+
duration_in_days = models.PositiveIntegerField(
|
|
121
119
|
blank=True,
|
|
122
|
-
|
|
120
|
+
null=True,
|
|
121
|
+
verbose_name=_("Duration (days)"),
|
|
122
|
+
help_text=_(
|
|
123
|
+
"Approximate time (in days) it should take to complete this Learning Path."
|
|
124
|
+
),
|
|
123
125
|
)
|
|
124
126
|
sequential = models.BooleanField(
|
|
125
127
|
default=False,
|
|
126
128
|
verbose_name=_("Is sequential"),
|
|
127
|
-
help_text=_(
|
|
129
|
+
help_text=_(
|
|
130
|
+
"Whether the courses in this Learning Path are meant to be taken sequentially."
|
|
131
|
+
),
|
|
128
132
|
)
|
|
129
133
|
# Note: the enrolled learners will be able to self-enroll in all courses
|
|
130
134
|
# (steps) of the learning path. To avoid mistakes of making the courses
|
|
@@ -133,18 +137,15 @@ class LearningPath(TimeStampedModel):
|
|
|
133
137
|
invite_only = models.BooleanField(
|
|
134
138
|
default=True,
|
|
135
139
|
verbose_name=_("Invite only"),
|
|
136
|
-
help_text=_(
|
|
140
|
+
help_text=_(
|
|
141
|
+
"If enabled, only staff can enroll users and only enrolled users can see the learning path."
|
|
142
|
+
),
|
|
137
143
|
)
|
|
138
144
|
enrolled_users = models.ManyToManyField(User, through="LearningPathEnrollment")
|
|
139
145
|
tracker = FieldTracker(fields=["image"])
|
|
140
146
|
|
|
141
147
|
objects = LearningPathManager()
|
|
142
148
|
|
|
143
|
-
steps: "models.Manager[LearningPathStep]"
|
|
144
|
-
requiredskill_set: "models.Manager[RequiredSkill]"
|
|
145
|
-
acquiredskill_set: "models.Manager[AcquiredSkill]"
|
|
146
|
-
grading_criteria: "LearningPathGradingCriteria"
|
|
147
|
-
|
|
148
149
|
def __str__(self):
|
|
149
150
|
"""User-friendly string representation of this model."""
|
|
150
151
|
return str(self.key)
|
|
@@ -197,12 +198,16 @@ class LearningPathStep(TimeStampedModel):
|
|
|
197
198
|
unique_together = ("learning_path", "course_key")
|
|
198
199
|
|
|
199
200
|
course_key = CourseKeyField(max_length=255)
|
|
200
|
-
learning_path = models.ForeignKey(
|
|
201
|
+
learning_path = models.ForeignKey(
|
|
202
|
+
LearningPath, related_name="steps", on_delete=models.CASCADE
|
|
203
|
+
)
|
|
201
204
|
order = models.PositiveIntegerField(
|
|
202
205
|
blank=True,
|
|
203
206
|
null=True,
|
|
204
207
|
verbose_name=_("Sequential order"),
|
|
205
|
-
help_text=_(
|
|
208
|
+
help_text=_(
|
|
209
|
+
"Ordinal position of this step in the sequence of the Learning Path, if applicable."
|
|
210
|
+
),
|
|
206
211
|
)
|
|
207
212
|
weight = models.FloatField(
|
|
208
213
|
default=1.0,
|
|
@@ -260,9 +265,7 @@ class LearningPathSkill(TimeStampedModel):
|
|
|
260
265
|
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
261
266
|
skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
|
|
262
267
|
level = models.PositiveIntegerField(
|
|
263
|
-
|
|
264
|
-
null=True,
|
|
265
|
-
help_text=_("The skill level associated with this course."),
|
|
268
|
+
help_text=_("The skill level associated with this course.")
|
|
266
269
|
)
|
|
267
270
|
|
|
268
271
|
def __str__(self):
|
|
@@ -310,6 +313,13 @@ class LearningPathEnrollment(TimeStampedModel):
|
|
|
310
313
|
"""User-friendly string representation of this model."""
|
|
311
314
|
return "{}: {}".format(self.user, self.learning_path)
|
|
312
315
|
|
|
316
|
+
@property
|
|
317
|
+
def estimated_end_date(self):
|
|
318
|
+
"""Estimated end date of the learning path."""
|
|
319
|
+
if self.learning_path.duration_in_days is None:
|
|
320
|
+
return None
|
|
321
|
+
return self.created + timedelta(days=self.learning_path.duration_in_days)
|
|
322
|
+
|
|
313
323
|
|
|
314
324
|
class LearningPathGradingCriteria(models.Model):
|
|
315
325
|
"""
|
|
@@ -318,7 +328,9 @@ class LearningPathGradingCriteria(models.Model):
|
|
|
318
328
|
.. no_pii:
|
|
319
329
|
"""
|
|
320
330
|
|
|
321
|
-
learning_path = models.OneToOneField(
|
|
331
|
+
learning_path = models.OneToOneField(
|
|
332
|
+
LearningPath, related_name="grading_criteria", on_delete=models.CASCADE
|
|
333
|
+
)
|
|
322
334
|
required_completion = models.FloatField(
|
|
323
335
|
default=0.80,
|
|
324
336
|
help_text=(
|
|
@@ -413,7 +425,9 @@ class LearningPathEnrollmentAudit(TimeStampedModel):
|
|
|
413
425
|
(DEFAULT_TRANSITION_STATE, DEFAULT_TRANSITION_STATE),
|
|
414
426
|
)
|
|
415
427
|
|
|
416
|
-
enrolled_by = models.ForeignKey(
|
|
428
|
+
enrolled_by = models.ForeignKey(
|
|
429
|
+
User, on_delete=models.CASCADE, null=True, related_name="learning_path_audit"
|
|
430
|
+
)
|
|
417
431
|
enrollment = models.ForeignKey(
|
|
418
432
|
LearningPathEnrollment,
|
|
419
433
|
on_delete=models.CASCADE,
|
|
@@ -426,7 +440,9 @@ class LearningPathEnrollmentAudit(TimeStampedModel):
|
|
|
426
440
|
null=True,
|
|
427
441
|
related_name="audit",
|
|
428
442
|
)
|
|
429
|
-
state_transition = models.CharField(
|
|
443
|
+
state_transition = models.CharField(
|
|
444
|
+
max_length=255, choices=TRANSITION_STATES, default=DEFAULT_TRANSITION_STATE
|
|
445
|
+
)
|
|
430
446
|
reason = models.TextField(blank=True)
|
|
431
447
|
org = models.CharField(max_length=255, blank=True, db_index=True)
|
|
432
448
|
role = models.CharField(max_length=255, blank=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-paths-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4rc8
|
|
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,6 @@ 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-object-actions
|
|
22
21
|
Requires-Dist: djangorestframework
|
|
23
22
|
Requires-Dist: edx-django-utils
|
|
24
23
|
Requires-Dist: edx-opaque-keys
|
|
@@ -121,7 +120,7 @@ Unreleased
|
|
|
121
120
|
|
|
122
121
|
*
|
|
123
122
|
|
|
124
|
-
0.3.4 - 2025-
|
|
123
|
+
0.3.4 - 2025-05-23
|
|
125
124
|
******************
|
|
126
125
|
|
|
127
126
|
Added
|
|
@@ -129,16 +128,12 @@ Added
|
|
|
129
128
|
|
|
130
129
|
* Bulk unenrollment API.
|
|
131
130
|
* 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
131
|
|
|
135
132
|
Changed
|
|
136
133
|
=======
|
|
137
134
|
|
|
138
135
|
* The Learning Paths API includes start and end dates for its steps.
|
|
139
136
|
* 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
137
|
|
|
143
138
|
0.3.3 - 2025-05-23
|
|
144
139
|
******************
|
|
@@ -37,8 +37,6 @@ learning_paths/migrations/0010_learningpath_invite_only.py
|
|
|
37
37
|
learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py
|
|
38
38
|
learning_paths/migrations/0012_alter_learningpath_subtitle.py
|
|
39
39
|
learning_paths/migrations/0013_enrollment_audit.py
|
|
40
|
-
learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py
|
|
41
|
-
learning_paths/migrations/0015_make_skill_level_optional.py
|
|
42
40
|
learning_paths/migrations/__init__.py
|
|
43
41
|
learning_paths/templates/learning_paths/base.html
|
|
44
42
|
learning_paths_plugin.egg-info/PKG-INFO
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# Generated by Django 4.2.20 on 2025-07-14 13:10
|
|
2
|
-
|
|
3
|
-
from django.db import migrations, models
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def transfer_duration_data(apps, schema_editor):
|
|
7
|
-
"""Transfer duration_in_days values to duration field with '{x} days' format."""
|
|
8
|
-
LearningPath = apps.get_model("learning_paths", "LearningPath")
|
|
9
|
-
|
|
10
|
-
for learning_path in LearningPath.objects.filter(duration_in_days__isnull=False):
|
|
11
|
-
learning_path.duration = f"{learning_path.duration_in_days} days"
|
|
12
|
-
learning_path.save()
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def reverse_transfer_duration_data(apps, schema_editor):
|
|
16
|
-
"""Reverse operation: extract numeric values from duration field back to duration_in_days."""
|
|
17
|
-
LearningPath = apps.get_model("learning_paths", "LearningPath")
|
|
18
|
-
|
|
19
|
-
for learning_path in LearningPath.objects.filter(duration__endswith=" days"):
|
|
20
|
-
try:
|
|
21
|
-
days_str = learning_path.duration.replace(" days", "")
|
|
22
|
-
learning_path.duration_in_days = int(days_str)
|
|
23
|
-
learning_path.save()
|
|
24
|
-
except ValueError:
|
|
25
|
-
# Skip entries that don't match the expected format.
|
|
26
|
-
pass
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class Migration(migrations.Migration):
|
|
30
|
-
dependencies = [
|
|
31
|
-
("learning_paths", "0013_enrollment_audit"),
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
operations = [
|
|
35
|
-
migrations.AddField(
|
|
36
|
-
model_name="learningpath",
|
|
37
|
-
name="duration",
|
|
38
|
-
field=models.CharField(
|
|
39
|
-
blank=True,
|
|
40
|
-
help_text="Approximate time it should take to complete this Learning Path. Example: '10 Weeks'.",
|
|
41
|
-
max_length=255,
|
|
42
|
-
),
|
|
43
|
-
),
|
|
44
|
-
migrations.AddField(
|
|
45
|
-
model_name="learningpath",
|
|
46
|
-
name="time_commitment",
|
|
47
|
-
field=models.CharField(
|
|
48
|
-
blank=True, help_text="Approximate time commitment. Example: '4-6 hours/week'.", max_length=255
|
|
49
|
-
),
|
|
50
|
-
),
|
|
51
|
-
migrations.RunPython(
|
|
52
|
-
transfer_duration_data,
|
|
53
|
-
reverse_transfer_duration_data,
|
|
54
|
-
),
|
|
55
|
-
migrations.RemoveField(
|
|
56
|
-
model_name="learningpath",
|
|
57
|
-
name="duration_in_days",
|
|
58
|
-
),
|
|
59
|
-
]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Generated by Django 4.2.23 on 2025-07-22 22:28
|
|
2
|
-
|
|
3
|
-
from django.db import migrations, models
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Migration(migrations.Migration):
|
|
7
|
-
|
|
8
|
-
dependencies = [
|
|
9
|
-
("learning_paths", "0014_remove_learningpath_duration_in_days_and_more"),
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
operations = [
|
|
13
|
-
migrations.AlterField(
|
|
14
|
-
model_name="acquiredskill",
|
|
15
|
-
name="level",
|
|
16
|
-
field=models.PositiveIntegerField(
|
|
17
|
-
blank=True, help_text="The skill level associated with this course.", null=True
|
|
18
|
-
),
|
|
19
|
-
),
|
|
20
|
-
migrations.AlterField(
|
|
21
|
-
model_name="requiredskill",
|
|
22
|
-
name="level",
|
|
23
|
-
field=models.PositiveIntegerField(
|
|
24
|
-
blank=True, help_text="The skill level associated with this course.", null=True
|
|
25
|
-
),
|
|
26
|
-
),
|
|
27
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/filters.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/permissions.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|