learning-paths-plugin 0.3.4rc7__tar.gz → 0.3.4rc9__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.4rc7 → learning_paths_plugin-0.3.4rc9}/CHANGELOG.rst +4 -0
- {learning_paths_plugin-0.3.4rc7/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc9}/PKG-INFO +6 -1
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/__init__.py +1 -1
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/admin.py +78 -3
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/serializers.py +2 -1
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/views.py +0 -1
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0013_enrollment_audit.py +11 -0
- learning_paths_plugin-0.3.4rc9/learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py +59 -0
- learning_paths_plugin-0.3.4rc9/learning_paths/migrations/0015_make_skill_level_optional.py +27 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/models.py +35 -19
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9/learning_paths_plugin.egg-info}/PKG-INFO +6 -1
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/SOURCES.txt +2 -2
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/requires.txt +1 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/requirements/base.in +1 -0
- learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0014_learningpathenrollmentallowed_is_active.py +0 -20
- learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0015_remove_learningpathenrollment_enrolled_at.py +0 -17
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/LICENSE.txt +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/MANIFEST.in +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/README.rst +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/filters.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/permissions.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/utils.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/apps.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/compat.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/conftest.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/keys.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0001_initial.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0006_enrollment_models.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/receivers.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/settings.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/templates/learning_paths/base.html +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/top_level.txt +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/pyproject.toml +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/requirements/constraints.txt +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/setup.cfg +0 -0
- {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/setup.py +0 -0
|
@@ -24,12 +24,16 @@ 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.
|
|
27
29
|
|
|
28
30
|
Changed
|
|
29
31
|
=======
|
|
30
32
|
|
|
31
33
|
* The Learning Paths API includes start and end dates for its steps.
|
|
32
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.
|
|
33
37
|
|
|
34
38
|
0.3.3 - 2025-05-23
|
|
35
39
|
******************
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-paths-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4rc9
|
|
4
4
|
Summary: Learning Paths plugin
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-paths-plugin
|
|
6
6
|
Author: OpenCraft
|
|
@@ -18,6 +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-object-actions
|
|
21
22
|
Requires-Dist: djangorestframework
|
|
22
23
|
Requires-Dist: edx-django-utils
|
|
23
24
|
Requires-Dist: edx-opaque-keys
|
|
@@ -128,12 +129,16 @@ Added
|
|
|
128
129
|
|
|
129
130
|
* Bulk unenrollment API.
|
|
130
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.
|
|
131
134
|
|
|
132
135
|
Changed
|
|
133
136
|
=======
|
|
134
137
|
|
|
135
138
|
* The Learning Paths API includes start and end dates for its steps.
|
|
136
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.
|
|
137
142
|
|
|
138
143
|
0.3.3 - 2025-05-23
|
|
139
144
|
******************
|
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
Django Admin for learning_paths.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
|
|
5
7
|
from django import forms
|
|
6
|
-
from django.contrib import admin, auth
|
|
8
|
+
from django.contrib import admin, auth, messages
|
|
7
9
|
from django.core.exceptions import ValidationError
|
|
10
|
+
from django.core.files.base import ContentFile
|
|
8
11
|
from django.db import transaction
|
|
12
|
+
from django.http import HttpResponseRedirect
|
|
13
|
+
from django.urls import reverse
|
|
9
14
|
from django.utils.translation import gettext_lazy as _
|
|
15
|
+
from django_object_actions import DjangoObjectActions, action
|
|
10
16
|
|
|
11
17
|
from .compat import get_course_keys_with_outlines
|
|
12
18
|
from .models import (
|
|
@@ -105,6 +111,7 @@ class LearningPathGradingCriteriaInline(admin.TabularInline):
|
|
|
105
111
|
"""Inline Admin for Learning path grading criteria."""
|
|
106
112
|
|
|
107
113
|
model = LearningPathGradingCriteria
|
|
114
|
+
verbose_name = "Certificate Criteria"
|
|
108
115
|
|
|
109
116
|
|
|
110
117
|
class BulkEnrollUsersForm(forms.ModelForm):
|
|
@@ -138,7 +145,7 @@ class BulkEnrollUsersForm(forms.ModelForm):
|
|
|
138
145
|
|
|
139
146
|
|
|
140
147
|
@admin.register(LearningPath)
|
|
141
|
-
class LearningPathAdmin(admin.ModelAdmin):
|
|
148
|
+
class LearningPathAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|
142
149
|
"""Admin for Learning Path."""
|
|
143
150
|
|
|
144
151
|
model = LearningPath
|
|
@@ -152,7 +159,7 @@ class LearningPathAdmin(admin.ModelAdmin):
|
|
|
152
159
|
"key",
|
|
153
160
|
"display_name",
|
|
154
161
|
"level",
|
|
155
|
-
"
|
|
162
|
+
"duration",
|
|
156
163
|
"invite_only",
|
|
157
164
|
)
|
|
158
165
|
list_filter = ("invite_only",)
|
|
@@ -165,6 +172,8 @@ class LearningPathAdmin(admin.ModelAdmin):
|
|
|
165
172
|
LearningPathGradingCriteriaInline,
|
|
166
173
|
]
|
|
167
174
|
|
|
175
|
+
change_actions = ("duplicate_learning_path",)
|
|
176
|
+
|
|
168
177
|
def get_readonly_fields(self, request, obj=None):
|
|
169
178
|
"""Make key read-only only for existing objects."""
|
|
170
179
|
if obj: # Editing an existing object.
|
|
@@ -178,6 +187,72 @@ class LearningPathAdmin(admin.ModelAdmin):
|
|
|
178
187
|
for user in form.cleaned_data["usernames"]:
|
|
179
188
|
LearningPathEnrollment.objects.get_or_create(user=user, learning_path=form.instance)
|
|
180
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(
|
|
220
|
+
original_filename,
|
|
221
|
+
ContentFile(image_content),
|
|
222
|
+
save=False
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
new_learning_path.save()
|
|
226
|
+
|
|
227
|
+
new_learning_path.refresh_from_db()
|
|
228
|
+
new_learning_path.grading_criteria.required_completion = obj.grading_criteria.required_completion
|
|
229
|
+
new_learning_path.grading_criteria.required_grade = obj.grading_criteria.required_grade
|
|
230
|
+
new_learning_path.grading_criteria.save()
|
|
231
|
+
|
|
232
|
+
for step in obj.steps.all():
|
|
233
|
+
step.pk = None
|
|
234
|
+
step.learning_path = new_learning_path
|
|
235
|
+
step.save()
|
|
236
|
+
|
|
237
|
+
for skill in obj.requiredskill_set.all():
|
|
238
|
+
skill.pk = None
|
|
239
|
+
skill.learning_path = new_learning_path
|
|
240
|
+
skill.save()
|
|
241
|
+
|
|
242
|
+
for skill in obj.acquiredskill_set.all():
|
|
243
|
+
skill.pk = None
|
|
244
|
+
skill.learning_path = new_learning_path
|
|
245
|
+
skill.save()
|
|
246
|
+
|
|
247
|
+
messages.success(
|
|
248
|
+
request,
|
|
249
|
+
f'Learning path duplicated successfully. New key: {new_key}'
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
return HttpResponseRedirect(
|
|
253
|
+
reverse('admin:learning_paths_learningpath_change', args=[new_learning_path.pk])
|
|
254
|
+
)
|
|
255
|
+
|
|
181
256
|
|
|
182
257
|
@admin.register(Skill)
|
|
183
258
|
class SkillAdmin(admin.ModelAdmin):
|
|
@@ -155,4 +155,15 @@ class Migration(migrations.Migration):
|
|
|
155
155
|
"abstract": False,
|
|
156
156
|
},
|
|
157
157
|
),
|
|
158
|
+
migrations.AddField(
|
|
159
|
+
model_name="learningpathenrollmentallowed",
|
|
160
|
+
name="is_active",
|
|
161
|
+
field=models.BooleanField(
|
|
162
|
+
db_index=True, default=True, help_text="Indicates if the enrollment allowance is active"
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
migrations.RemoveField(
|
|
166
|
+
model_name="learningpathenrollment",
|
|
167
|
+
name="enrolled_at",
|
|
168
|
+
),
|
|
158
169
|
]
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
]
|
|
@@ -5,14 +5,14 @@ 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
|
|
9
9
|
from uuid import uuid4
|
|
10
10
|
|
|
11
11
|
from django.contrib import auth
|
|
12
12
|
from django.core.exceptions import ValidationError
|
|
13
13
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
14
14
|
from django.db import models
|
|
15
|
-
from django.db.models import
|
|
15
|
+
from django.db.models import OuterRef, Q
|
|
16
16
|
from django.utils.translation import gettext_lazy as _
|
|
17
17
|
from model_utils import FieldTracker
|
|
18
18
|
from model_utils.models import TimeStampedModel
|
|
@@ -60,7 +60,7 @@ class LearningPathManager(models.Manager):
|
|
|
60
60
|
queryset = queryset.filter(Q(invite_only=False) | Q(enrollment_date__isnull=False))
|
|
61
61
|
|
|
62
62
|
# Order by enrollment date (the most recent first), with null values at the end.
|
|
63
|
-
return queryset.order_by(models.F(
|
|
63
|
+
return queryset.order_by(models.F("enrollment_date").desc(nulls_last=True))
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class LearningPath(TimeStampedModel):
|
|
@@ -104,18 +104,22 @@ class LearningPath(TimeStampedModel):
|
|
|
104
104
|
subtitle = models.TextField(blank=True)
|
|
105
105
|
description = models.TextField(blank=True)
|
|
106
106
|
image = models.ImageField(
|
|
107
|
-
upload_to=_learning_path_image_upload_path,
|
|
107
|
+
upload_to=_learning_path_image_upload_path, # type: ignore
|
|
108
108
|
blank=True,
|
|
109
109
|
null=True,
|
|
110
110
|
verbose_name=_("Image"),
|
|
111
111
|
help_text=_("Image representing this Learning Path."),
|
|
112
112
|
)
|
|
113
113
|
level = models.CharField(max_length=255, blank=True, choices=LEVEL_CHOICES)
|
|
114
|
-
|
|
114
|
+
duration = models.CharField(
|
|
115
|
+
max_length=255,
|
|
115
116
|
blank=True,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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,
|
|
121
|
+
blank=True,
|
|
122
|
+
help_text=_("Approximate time commitment. Example: '4-6 hours/week'."),
|
|
119
123
|
)
|
|
120
124
|
sequential = models.BooleanField(
|
|
121
125
|
default=False,
|
|
@@ -136,6 +140,11 @@ class LearningPath(TimeStampedModel):
|
|
|
136
140
|
|
|
137
141
|
objects = LearningPathManager()
|
|
138
142
|
|
|
143
|
+
steps: "models.Manager[LearningPathStep]"
|
|
144
|
+
requiredskill_set: "models.Manager[RequiredSkill]"
|
|
145
|
+
acquiredskill_set: "models.Manager[AcquiredSkill]"
|
|
146
|
+
grading_criteria: "LearningPathGradingCriteria"
|
|
147
|
+
|
|
139
148
|
def __str__(self):
|
|
140
149
|
"""User-friendly string representation of this model."""
|
|
141
150
|
return str(self.key)
|
|
@@ -250,7 +259,11 @@ class LearningPathSkill(TimeStampedModel):
|
|
|
250
259
|
|
|
251
260
|
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
252
261
|
skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
|
|
253
|
-
level = models.PositiveIntegerField(
|
|
262
|
+
level = models.PositiveIntegerField(
|
|
263
|
+
blank=True,
|
|
264
|
+
null=True,
|
|
265
|
+
help_text=_("The skill level associated with this course."),
|
|
266
|
+
)
|
|
254
267
|
|
|
255
268
|
def __str__(self):
|
|
256
269
|
"""User-friendly string representation of this model."""
|
|
@@ -297,13 +310,6 @@ class LearningPathEnrollment(TimeStampedModel):
|
|
|
297
310
|
"""User-friendly string representation of this model."""
|
|
298
311
|
return "{}: {}".format(self.user, self.learning_path)
|
|
299
312
|
|
|
300
|
-
@property
|
|
301
|
-
def estimated_end_date(self):
|
|
302
|
-
"""Estimated end date of the learning path."""
|
|
303
|
-
if self.learning_path.duration_in_days is None:
|
|
304
|
-
return None
|
|
305
|
-
return self.created + timedelta(days=self.learning_path.duration_in_days)
|
|
306
|
-
|
|
307
313
|
|
|
308
314
|
class LearningPathGradingCriteria(models.Model):
|
|
309
315
|
"""
|
|
@@ -369,7 +375,9 @@ class LearningPathEnrollmentAllowed(TimeStampedModel):
|
|
|
369
375
|
learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
|
|
370
376
|
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
|
|
371
377
|
is_active = models.BooleanField(
|
|
372
|
-
default=True,
|
|
378
|
+
default=True,
|
|
379
|
+
db_index=True,
|
|
380
|
+
help_text=_("Indicates if the enrollment allowance is active"),
|
|
373
381
|
)
|
|
374
382
|
|
|
375
383
|
def __str__(self):
|
|
@@ -406,9 +414,17 @@ class LearningPathEnrollmentAudit(TimeStampedModel):
|
|
|
406
414
|
)
|
|
407
415
|
|
|
408
416
|
enrolled_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name="learning_path_audit")
|
|
409
|
-
enrollment = models.ForeignKey(
|
|
417
|
+
enrollment = models.ForeignKey(
|
|
418
|
+
LearningPathEnrollment,
|
|
419
|
+
on_delete=models.CASCADE,
|
|
420
|
+
null=True,
|
|
421
|
+
related_name="audit",
|
|
422
|
+
)
|
|
410
423
|
enrollment_allowed = models.ForeignKey(
|
|
411
|
-
LearningPathEnrollmentAllowed,
|
|
424
|
+
LearningPathEnrollmentAllowed,
|
|
425
|
+
on_delete=models.CASCADE,
|
|
426
|
+
null=True,
|
|
427
|
+
related_name="audit",
|
|
412
428
|
)
|
|
413
429
|
state_transition = models.CharField(max_length=255, choices=TRANSITION_STATES, default=DEFAULT_TRANSITION_STATE)
|
|
414
430
|
reason = models.TextField(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.4rc9
|
|
4
4
|
Summary: Learning Paths plugin
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-paths-plugin
|
|
6
6
|
Author: OpenCraft
|
|
@@ -18,6 +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-object-actions
|
|
21
22
|
Requires-Dist: djangorestframework
|
|
22
23
|
Requires-Dist: edx-django-utils
|
|
23
24
|
Requires-Dist: edx-opaque-keys
|
|
@@ -128,12 +129,16 @@ Added
|
|
|
128
129
|
|
|
129
130
|
* Bulk unenrollment API.
|
|
130
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.
|
|
131
134
|
|
|
132
135
|
Changed
|
|
133
136
|
=======
|
|
134
137
|
|
|
135
138
|
* The Learning Paths API includes start and end dates for its steps.
|
|
136
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.
|
|
137
142
|
|
|
138
143
|
0.3.3 - 2025-05-23
|
|
139
144
|
******************
|
|
@@ -37,8 +37,8 @@ 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/
|
|
41
|
-
learning_paths/migrations/
|
|
40
|
+
learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py
|
|
41
|
+
learning_paths/migrations/0015_make_skill_level_optional.py
|
|
42
42
|
learning_paths/migrations/__init__.py
|
|
43
43
|
learning_paths/templates/learning_paths/base.html
|
|
44
44
|
learning_paths_plugin.egg-info/PKG-INFO
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# Generated by Django 4.2.20 on 2025-05-30 19:57
|
|
2
|
-
|
|
3
|
-
from django.db import migrations, models
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Migration(migrations.Migration):
|
|
7
|
-
|
|
8
|
-
dependencies = [
|
|
9
|
-
("learning_paths", "0013_enrollment_audit"),
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
operations = [
|
|
13
|
-
migrations.AddField(
|
|
14
|
-
model_name="learningpathenrollmentallowed",
|
|
15
|
-
name="is_active",
|
|
16
|
-
field=models.BooleanField(
|
|
17
|
-
db_index=True, default=True, help_text="Indicates if the enrollment allowance is active"
|
|
18
|
-
),
|
|
19
|
-
),
|
|
20
|
-
]
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Generated by Django 4.2.20 on 2025-06-02 14:39
|
|
2
|
-
|
|
3
|
-
from django.db import migrations
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Migration(migrations.Migration):
|
|
7
|
-
|
|
8
|
-
dependencies = [
|
|
9
|
-
("learning_paths", "0014_learningpathenrollmentallowed_is_active"),
|
|
10
|
-
]
|
|
11
|
-
|
|
12
|
-
operations = [
|
|
13
|
-
migrations.RemoveField(
|
|
14
|
-
model_name="learningpathenrollment",
|
|
15
|
-
name="enrolled_at",
|
|
16
|
-
),
|
|
17
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/filters.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/conftest.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
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/receivers.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/settings.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
|
{learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/requirements/constraints.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|