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.
Files changed (55) hide show
  1. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/CHANGELOG.rst +4 -0
  2. {learning_paths_plugin-0.3.4rc7/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc9}/PKG-INFO +6 -1
  3. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/__init__.py +1 -1
  4. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/admin.py +78 -3
  5. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/serializers.py +2 -1
  6. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/views.py +0 -1
  7. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0013_enrollment_audit.py +11 -0
  8. learning_paths_plugin-0.3.4rc9/learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py +59 -0
  9. learning_paths_plugin-0.3.4rc9/learning_paths/migrations/0015_make_skill_level_optional.py +27 -0
  10. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/models.py +35 -19
  11. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9/learning_paths_plugin.egg-info}/PKG-INFO +6 -1
  12. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/SOURCES.txt +2 -2
  13. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/requires.txt +1 -0
  14. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/requirements/base.in +1 -0
  15. learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0014_learningpathenrollmentallowed_is_active.py +0 -20
  16. learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0015_remove_learningpathenrollment_enrolled_at.py +0 -17
  17. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/LICENSE.txt +0 -0
  18. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/MANIFEST.in +0 -0
  19. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/README.rst +0 -0
  20. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/__init__.py +0 -0
  21. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/urls.py +0 -0
  22. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/__init__.py +0 -0
  23. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/filters.py +0 -0
  24. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/permissions.py +0 -0
  25. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/urls.py +0 -0
  26. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/api/v1/utils.py +0 -0
  27. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/apps.py +0 -0
  28. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/compat.py +0 -0
  29. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/conftest.py +0 -0
  30. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/keys.py +0 -0
  31. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0001_initial.py +0 -0
  32. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
  33. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
  34. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
  35. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
  36. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0006_enrollment_models.py +0 -0
  37. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
  38. {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
  39. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
  40. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
  41. {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
  42. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
  43. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/migrations/__init__.py +0 -0
  44. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/receivers.py +0 -0
  45. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/settings.py +0 -0
  46. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/templates/learning_paths/base.html +0 -0
  47. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths/urls.py +0 -0
  48. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
  49. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
  50. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
  51. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/learning_paths_plugin.egg-info/top_level.txt +0 -0
  52. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/pyproject.toml +0 -0
  53. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/requirements/constraints.txt +0 -0
  54. {learning_paths_plugin-0.3.4rc7 → learning_paths_plugin-0.3.4rc9}/setup.cfg +0 -0
  55. {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.4rc7
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,4 +2,4 @@
2
2
  Learning Paths plugin.
3
3
  """
4
4
 
5
- __version__ = "0.3.4-rc7"
5
+ __version__ = "0.3.4-rc9"
@@ -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
- "duration_in_days",
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):
@@ -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
  ]
@@ -3,7 +3,6 @@ Views for LearningPath.
3
3
  """
4
4
 
5
5
  import logging
6
- from datetime import datetime, timezone
7
6
 
8
7
  from django.conf import settings
9
8
  from django.contrib.auth import get_user_model
@@ -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, timedelta
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 Exists, OuterRef, Q
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('enrollment_date').desc(nulls_last=True))
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
- duration_in_days = models.PositiveIntegerField(
114
+ duration = models.CharField(
115
+ max_length=255,
115
116
  blank=True,
116
- null=True,
117
- verbose_name=_("Duration (days)"),
118
- help_text=_("Approximate time (in days) it should take to complete this Learning Path."),
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(help_text=_("The skill level associated with this course."))
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, db_index=True, help_text=_("Indicates if the enrollment allowance is active")
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(LearningPathEnrollment, on_delete=models.CASCADE, null=True, related_name="audit")
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, on_delete=models.CASCADE, null=True, related_name="audit"
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.4rc7
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/0014_learningpathenrollmentallowed_is_active.py
41
- learning_paths/migrations/0015_remove_learningpathenrollment_enrolled_at.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
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,5 +1,6 @@
1
1
  Django
2
2
  django-model-utils
3
+ django-object-actions
3
4
  djangorestframework
4
5
  edx-django-utils
5
6
  edx-opaque-keys
@@ -4,6 +4,7 @@
4
4
  Django # Web application framework
5
5
 
6
6
  django-model-utils
7
+ django-object-actions # Add action buttons to Django admin
7
8
  djangorestframework
8
9
  edx-django-utils
9
10
  edx-opaque-keys
@@ -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
- ]