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.
Files changed (53) hide show
  1. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/CHANGELOG.rst +1 -5
  2. {learning_paths_plugin-0.3.4/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc8}/PKG-INFO +2 -7
  3. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/__init__.py +1 -1
  4. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/admin.py +3 -68
  5. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/serializers.py +1 -2
  6. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/views.py +1 -2
  7. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/models.py +43 -27
  8. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8/learning_paths_plugin.egg-info}/PKG-INFO +2 -7
  9. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/SOURCES.txt +0 -2
  10. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/requires.txt +0 -1
  11. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/requirements/base.in +0 -1
  12. learning_paths_plugin-0.3.4/learning_paths/migrations/0014_remove_learningpath_duration_in_days_and_more.py +0 -59
  13. learning_paths_plugin-0.3.4/learning_paths/migrations/0015_make_skill_level_optional.py +0 -27
  14. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/LICENSE.txt +0 -0
  15. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/MANIFEST.in +0 -0
  16. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/README.rst +0 -0
  17. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/__init__.py +0 -0
  18. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/urls.py +0 -0
  19. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/__init__.py +0 -0
  20. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/filters.py +0 -0
  21. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/permissions.py +0 -0
  22. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/urls.py +0 -0
  23. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/api/v1/utils.py +0 -0
  24. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/apps.py +0 -0
  25. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/compat.py +0 -0
  26. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/conftest.py +0 -0
  27. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/keys.py +0 -0
  28. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0001_initial.py +0 -0
  29. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
  30. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
  31. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
  32. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
  33. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0006_enrollment_models.py +0 -0
  34. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
  35. {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
  36. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
  37. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
  38. {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
  39. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
  40. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/0013_enrollment_audit.py +0 -0
  41. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/migrations/__init__.py +0 -0
  42. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/receivers.py +0 -0
  43. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/settings.py +0 -0
  44. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/templates/learning_paths/base.html +0 -0
  45. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths/urls.py +0 -0
  46. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
  47. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
  48. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
  49. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/learning_paths_plugin.egg-info/top_level.txt +0 -0
  50. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/pyproject.toml +0 -0
  51. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/requirements/constraints.txt +0 -0
  52. {learning_paths_plugin-0.3.4 → learning_paths_plugin-0.3.4rc8}/setup.cfg +0 -0
  53. {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-08-02
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.4
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-08-02
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,4 +2,4 @@
2
2
  Learning Paths plugin.
3
3
  """
4
4
 
5
- __version__ = "0.3.4"
5
+ __version__ = "0.3.4-rc8"
@@ -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, messages
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(DjangoObjectActions, admin.ModelAdmin):
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
- "duration",
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):
@@ -174,8 +174,7 @@ class LearningPathDetailSerializer(LearningPathListSerializer):
174
174
  "subtitle",
175
175
  "description",
176
176
  "level",
177
- "duration",
178
- "time_commitment",
177
+ "duration_in_days",
179
178
  "required_skills",
180
179
  "acquired_skills",
181
180
  ]
@@ -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 or (not allowed.user and not allowed.is_active):
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(enrollment_date=models.Subquery(enrollment_subquery))
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(Q(invite_only=False) | Q(enrollment_date__isnull=False))
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, # type: ignore
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
- duration = models.CharField(
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
- help_text=_("Approximate time commitment. Example: '4-6 hours/week'."),
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=_("Whether the courses in this Learning Path are meant to be taken sequentially."),
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=_("If enabled, only staff can enroll users and only enrolled users can see the learning path."),
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(LearningPath, related_name="steps", on_delete=models.CASCADE)
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=_("Ordinal position of this step in the sequence of the Learning Path, if applicable."),
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
- blank=True,
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(LearningPath, related_name="grading_criteria", on_delete=models.CASCADE)
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(User, on_delete=models.CASCADE, null=True, related_name="learning_path_audit")
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(max_length=255, choices=TRANSITION_STATES, default=DEFAULT_TRANSITION_STATE)
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.4
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-08-02
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,6 +1,5 @@
1
1
  Django
2
2
  django-model-utils
3
- django-object-actions
4
3
  djangorestframework
5
4
  edx-django-utils
6
5
  edx-opaque-keys
@@ -4,7 +4,6 @@
4
4
  Django # Web application framework
5
5
 
6
6
  django-model-utils
7
- django-object-actions # Add action buttons to Django admin
8
7
  djangorestframework
9
8
  edx-django-utils
10
9
  edx-opaque-keys
@@ -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
- ]