learning-paths-plugin 0.3.4rc5__tar.gz → 0.3.4rc7__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.4rc5 → learning_paths_plugin-0.3.4rc7}/CHANGELOG.rst +1 -0
  2. {learning_paths_plugin-0.3.4rc5/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc7}/PKG-INFO +2 -1
  3. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/__init__.py +1 -1
  4. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/admin.py +1 -1
  5. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/serializers.py +7 -7
  6. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/views.py +28 -5
  7. learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0014_learningpathenrollmentallowed_is_active.py +20 -0
  8. learning_paths_plugin-0.3.4rc7/learning_paths/migrations/0015_remove_learningpathenrollment_enrolled_at.py +17 -0
  9. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/models.py +14 -15
  10. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/receivers.py +4 -1
  11. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7/learning_paths_plugin.egg-info}/PKG-INFO +2 -1
  12. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/SOURCES.txt +2 -0
  13. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/LICENSE.txt +0 -0
  14. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/MANIFEST.in +0 -0
  15. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/README.rst +0 -0
  16. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/__init__.py +0 -0
  17. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/urls.py +0 -0
  18. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/__init__.py +0 -0
  19. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/filters.py +0 -0
  20. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/permissions.py +0 -0
  21. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/urls.py +0 -0
  22. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/api/v1/utils.py +0 -0
  23. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/apps.py +0 -0
  24. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/compat.py +0 -0
  25. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/conftest.py +0 -0
  26. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/keys.py +0 -0
  27. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0001_initial.py +0 -0
  28. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
  29. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
  30. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
  31. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
  32. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0006_enrollment_models.py +0 -0
  33. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
  34. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +0 -0
  35. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
  36. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
  37. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
  38. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
  39. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/0013_enrollment_audit.py +0 -0
  40. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/migrations/__init__.py +0 -0
  41. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/settings.py +0 -0
  42. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/templates/learning_paths/base.html +0 -0
  43. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths/urls.py +0 -0
  44. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
  45. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
  46. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
  47. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/requires.txt +0 -0
  48. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/learning_paths_plugin.egg-info/top_level.txt +0 -0
  49. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/pyproject.toml +0 -0
  50. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/requirements/base.in +0 -0
  51. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/requirements/constraints.txt +0 -0
  52. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/setup.cfg +0 -0
  53. {learning_paths_plugin-0.3.4rc5 → learning_paths_plugin-0.3.4rc7}/setup.py +0 -0
@@ -29,6 +29,7 @@ Changed
29
29
  =======
30
30
 
31
31
  * The Learning Paths API includes start and end dates for its steps.
32
+ * Return enrollment date in the API instead of a boolean.
32
33
 
33
34
  0.3.3 - 2025-05-23
34
35
  ******************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-paths-plugin
3
- Version: 0.3.4rc5
3
+ Version: 0.3.4rc7
4
4
  Summary: Learning Paths plugin
5
5
  Home-page: https://github.com/open-craft/learning-paths-plugin
6
6
  Author: OpenCraft
@@ -133,6 +133,7 @@ Changed
133
133
  =======
134
134
 
135
135
  * The Learning Paths API includes start and end dates for its steps.
136
+ * Return enrollment date in the API instead of a boolean.
136
137
 
137
138
  0.3.3 - 2025-05-23
138
139
  ******************
@@ -2,4 +2,4 @@
2
2
  Learning Paths plugin.
3
3
  """
4
4
 
5
- __version__ = "0.3.4-rc5"
5
+ __version__ = "0.3.4-rc7"
@@ -249,8 +249,8 @@ class EnrolledUsersAdmin(admin.ModelAdmin):
249
249
  "id",
250
250
  "user",
251
251
  "learning_path",
252
- "enrolled_at",
253
252
  "is_active",
253
+ "created",
254
254
  ]
255
255
 
256
256
  list_filter = [
@@ -105,7 +105,7 @@ class LearningPathListSerializer(serializers.ModelSerializer):
105
105
 
106
106
  steps = LearningPathStepSerializer(many=True, read_only=True)
107
107
  required_completion = serializers.FloatField(source="grading_criteria.required_completion", read_only=True)
108
- is_enrolled = serializers.SerializerMethodField()
108
+ enrollment_date = serializers.SerializerMethodField()
109
109
  invite_only = serializers.BooleanField()
110
110
  image = serializers.ImageField(read_only=True)
111
111
 
@@ -118,17 +118,17 @@ class LearningPathListSerializer(serializers.ModelSerializer):
118
118
  "sequential",
119
119
  "steps",
120
120
  "required_completion",
121
- "is_enrolled",
121
+ "enrollment_date",
122
122
  "invite_only",
123
123
  ]
124
124
 
125
- def get_is_enrolled(self, obj):
125
+ def get_enrollment_date(self, obj):
126
126
  """
127
127
  Check if the current user is enrolled in this learning path.
128
128
  """
129
- if hasattr(obj, "is_enrolled"):
130
- return obj.is_enrolled
131
- return False
129
+ if hasattr(obj, "enrollment_date"):
130
+ return obj.enrollment_date
131
+ return None
132
132
 
133
133
 
134
134
  class SkillSerializer(serializers.ModelSerializer):
@@ -183,4 +183,4 @@ class LearningPathDetailSerializer(LearningPathListSerializer):
183
183
  class LearningPathEnrollmentSerializer(serializers.ModelSerializer):
184
184
  class Meta:
185
185
  model = LearningPathEnrollment
186
- fields = ("user", "learning_path", "is_active", "enrolled_at")
186
+ fields = ("user", "learning_path", "is_active", "created")
@@ -242,7 +242,6 @@ class LearningPathEnrollmentView(APIView):
242
242
  return Response({"detail": "Enrollment exists."}, status=status.HTTP_409_CONFLICT)
243
243
 
244
244
  enrollment.is_active = True
245
- enrollment.enrolled_at = datetime.now(timezone.utc)
246
245
  enrollment.save()
247
246
  return Response(LearningPathEnrollmentSerializer(enrollment).data)
248
247
 
@@ -389,7 +388,6 @@ class BulkEnrollView(APIView):
389
388
  if enrollment:
390
389
  if not enrollment.is_active:
391
390
  enrollment.is_active = True
392
- enrollment.enrolled_at = datetime.now(timezone.utc)
393
391
  enrolled_now = True
394
392
  else:
395
393
  audit_data["state_transition"] = LearningPathEnrollmentAudit.ENROLLED_TO_ENROLLED
@@ -452,12 +450,13 @@ class BulkEnrollView(APIView):
452
450
  `role` (str, optional): User role, used for audit.
453
451
 
454
452
  * For existing users, it deactivates their LearningPathEnrollment records.
455
- * Does not affect LearningPathEnrollmentAllowed records (allowed to enroll records).
453
+ * For emails with active LearningPathEnrollmentAllowed records, it deactivates those records.
456
454
 
457
455
  """
458
- learning_paths, existing_users, _ = self._setup_bulk_operation(request)
456
+ learning_paths, existing_users, emails = self._setup_bulk_operation(request)
459
457
 
460
458
  enrollments_unenrolled = []
459
+ enrollment_allowed_deactivated = []
461
460
 
462
461
  for learning_path in learning_paths:
463
462
  for user in existing_users:
@@ -474,9 +473,33 @@ class BulkEnrollView(APIView):
474
473
  enrollment._audit = audit_data # pylint: disable=protected-access
475
474
  enrollment.save()
476
475
 
476
+ for email in emails:
477
+ try:
478
+ validate_email(email)
479
+ except ValidationError:
480
+ logger.warning("BulkEnrollView: Invalid email: %s", email)
481
+ continue
482
+
483
+ enrollment_allowed = LearningPathEnrollmentAllowed.objects.filter(
484
+ email=email,
485
+ learning_path=learning_path,
486
+ ).first()
487
+
488
+ if enrollment_allowed:
489
+ if enrollment_allowed.is_active:
490
+ state_transition = LearningPathEnrollmentAudit.ALLOWEDTOENROLL_TO_UNENROLLED
491
+ enrollment_allowed.is_active = False
492
+ enrollment_allowed_deactivated.append(enrollment_allowed)
493
+ else:
494
+ state_transition = LearningPathEnrollmentAudit.UNENROLLED_TO_UNENROLLED
495
+ audit_data = self._create_audit_data(request, state_transition)
496
+ enrollment_allowed._audit = audit_data # pylint: disable=protected-access
497
+ enrollment_allowed.save()
498
+
477
499
  return Response(
478
500
  {
479
501
  "enrollments_unenrolled": len(enrollments_unenrolled),
502
+ "enrollment_allowed_deactivated": len(enrollment_allowed_deactivated),
480
503
  },
481
504
  status=status.HTTP_204_NO_CONTENT,
482
505
  )
@@ -494,7 +517,7 @@ class LearningPathCourseEnrollmentView(APIView):
494
517
  :raises: Http404 if the learning path is not found or the user does not have access.
495
518
  """
496
519
  return get_object_or_404(
497
- LearningPath.objects.get_paths_visible_to_user(self.request.user).filter(is_enrolled=True),
520
+ LearningPath.objects.get_paths_visible_to_user(self.request.user).filter(enrollment_date__isnull=False),
498
521
  key=learning_path_key_str,
499
522
  )
500
523
 
@@ -0,0 +1,20 @@
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
+ ]
@@ -0,0 +1,17 @@
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
+ ]
@@ -38,27 +38,29 @@ class LearningPathManager(models.Manager):
38
38
 
39
39
  def get_paths_visible_to_user(self, user: User) -> models.QuerySet:
40
40
  """
41
- Return only learning paths that should be visible to the given user with enrollment status.
41
+ Return only learning paths that should be visible to the given user with an enrollment date.
42
42
 
43
43
  For staff users: all learning paths.
44
44
  For non-staff: non-invite-only paths or invite-only paths they're enrolled in.
45
45
 
46
- Each learning path in the queryset is annotated with `is_enrolled` indicating
47
- whether the user has an active enrollment in that learning path.
46
+ Each learning path in the queryset is annotated with `enrollment_date` indicating
47
+ the date when the user enrolled in that learning path (None if not enrolled).
48
+ Results are ordered by enrollment date (the most recent first), with non-enrolled paths at the end.
48
49
  """
49
50
  queryset = self.get_queryset()
50
51
 
51
- # Annotate each path with whether the user is enrolled.
52
- enrollment_exists = LearningPathEnrollment.objects.filter(
52
+ # Annotate each path with the enrollment date.
53
+ enrollment_subquery = LearningPathEnrollment.objects.filter(
53
54
  learning_path=OuterRef("pk"), user=user, is_active=True
54
- )
55
- queryset = queryset.annotate(is_enrolled=Exists(enrollment_exists))
55
+ ).values("created")[:1]
56
+ queryset = queryset.annotate(enrollment_date=models.Subquery(enrollment_subquery))
56
57
 
57
58
  # Apply visibility filtering based on the user role.
58
59
  if not user.is_staff:
59
- queryset = queryset.filter(Q(invite_only=False) | Q(is_enrolled=True))
60
+ queryset = queryset.filter(Q(invite_only=False) | Q(enrollment_date__isnull=False))
60
61
 
61
- return queryset
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))
62
64
 
63
65
 
64
66
  class LearningPath(TimeStampedModel):
@@ -289,12 +291,6 @@ class LearningPathEnrollment(TimeStampedModel):
289
291
  default=True,
290
292
  help_text=_("Indicates if the learner is enrolled or not in the Learning Path"),
291
293
  )
292
- enrolled_at = models.DateTimeField(
293
- auto_now_add=True,
294
- help_text=_(
295
- "Timestamp of enrollment or un-enrollment. To be explicitly set when performing a learner enrollment."
296
- ),
297
- )
298
294
  tracker = FieldTracker(fields=["is_active"])
299
295
 
300
296
  def __str__(self):
@@ -372,6 +368,9 @@ class LearningPathEnrollmentAllowed(TimeStampedModel):
372
368
  email = models.EmailField(db_index=True)
373
369
  learning_path = models.ForeignKey(LearningPath, on_delete=models.CASCADE)
374
370
  user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
371
+ is_active = models.BooleanField(
372
+ default=True, db_index=True, help_text=_("Indicates if the enrollment allowance is active")
373
+ )
375
374
 
376
375
  def __str__(self):
377
376
  """User-friendly string representation of this model."""
@@ -39,7 +39,7 @@ def process_pending_enrollments(sender, instance, created, **kwargs):
39
39
  return
40
40
 
41
41
  logger.info("[LearningPaths] Processing pending enrollments for user %s", instance)
42
- pending_enrollments = LearningPathEnrollmentAllowed.objects.filter(email=instance.email)
42
+ pending_enrollments = LearningPathEnrollmentAllowed.objects.filter(email=instance.email, is_active=True)
43
43
  enrollments_created = 0
44
44
 
45
45
  for entry in pending_enrollments:
@@ -67,6 +67,7 @@ def process_pending_enrollments(sender, instance, created, **kwargs):
67
67
  entry.learning_path.key,
68
68
  )
69
69
  finally:
70
+ entry.is_active = False
70
71
  entry.user = instance
71
72
  entry.save()
72
73
 
@@ -129,6 +130,8 @@ def create_enrollment_allowed_audit(sender, instance, created, **kwargs):
129
130
  if not (audit_data := getattr(instance, "_audit", {})):
130
131
  return
131
132
 
133
+ audit_data.setdefault("state_transition", LearningPathEnrollmentAudit.UNENROLLED_TO_ALLOWEDTOENROLL)
134
+
132
135
  audit_data["state_transition"] = audit_data.get(
133
136
  "state_transition", LearningPathEnrollmentAudit.UNENROLLED_TO_ALLOWEDTOENROLL
134
137
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-paths-plugin
3
- Version: 0.3.4rc5
3
+ Version: 0.3.4rc7
4
4
  Summary: Learning Paths plugin
5
5
  Home-page: https://github.com/open-craft/learning-paths-plugin
6
6
  Author: OpenCraft
@@ -133,6 +133,7 @@ Changed
133
133
  =======
134
134
 
135
135
  * The Learning Paths API includes start and end dates for its steps.
136
+ * Return enrollment date in the API instead of a boolean.
136
137
 
137
138
  0.3.3 - 2025-05-23
138
139
  ******************
@@ -37,6 +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
42
  learning_paths/migrations/__init__.py
41
43
  learning_paths/templates/learning_paths/base.html
42
44
  learning_paths_plugin.egg-info/PKG-INFO