learning-paths-plugin 0.3.4rc2__tar.gz → 0.3.4rc3__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.4rc2 → learning_paths_plugin-0.3.4rc3}/CHANGELOG.rst +1 -0
- {learning_paths_plugin-0.3.4rc2/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc3}/PKG-INFO +2 -1
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/__init__.py +1 -1
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/views.py +108 -30
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3/learning_paths_plugin.egg-info}/PKG-INFO +2 -1
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/LICENSE.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/MANIFEST.in +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/README.rst +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/admin.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/filters.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/permissions.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/serializers.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/utils.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/apps.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/compat.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/conftest.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/keys.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0001_initial.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0006_enrollment_models.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0008_remove_learningpathstep_relative_due_date_in_days.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0013_enrollment_audit.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/models.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/receivers.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/settings.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/templates/learning_paths/base.html +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/SOURCES.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/requires.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/top_level.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/pyproject.toml +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/requirements/base.in +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/requirements/constraints.txt +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/setup.cfg +0 -0
- {learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-paths-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4rc3
|
|
4
4
|
Summary: Learning Paths plugin
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-paths-plugin
|
|
6
6
|
Author: OpenCraft
|
|
@@ -126,6 +126,7 @@ Unreleased
|
|
|
126
126
|
Added
|
|
127
127
|
=====
|
|
128
128
|
|
|
129
|
+
* Bulk unenrollment API.
|
|
129
130
|
* Enrollment audit model that tracks the enrollment state transitions.
|
|
130
131
|
|
|
131
132
|
0.3.3 - 2025-05-23
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/views.py
RENAMED
|
@@ -9,6 +9,7 @@ from django.conf import settings
|
|
|
9
9
|
from django.contrib.auth import get_user_model
|
|
10
10
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError
|
|
11
11
|
from django.core.validators import validate_email
|
|
12
|
+
from django.db.models import QuerySet
|
|
12
13
|
from django.shortcuts import get_object_or_404
|
|
13
14
|
from opaque_keys import InvalidKeyError
|
|
14
15
|
from opaque_keys.edx.keys import CourseKey
|
|
@@ -16,6 +17,7 @@ from rest_framework import generics, status, viewsets
|
|
|
16
17
|
from rest_framework.exceptions import NotFound, ParseError
|
|
17
18
|
from rest_framework.pagination import PageNumberPagination
|
|
18
19
|
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
|
20
|
+
from rest_framework.request import Request
|
|
19
21
|
from rest_framework.response import Response
|
|
20
22
|
from rest_framework.views import APIView
|
|
21
23
|
|
|
@@ -296,13 +298,53 @@ class ListEnrollmentsView(generics.ListAPIView):
|
|
|
296
298
|
|
|
297
299
|
class BulkEnrollView(APIView):
|
|
298
300
|
"""
|
|
299
|
-
Bulk enrollment API for LearningPathEnrollment.
|
|
300
|
-
|
|
301
|
+
Bulk enrollment/unenrollment API for LearningPathEnrollment.
|
|
301
302
|
"""
|
|
302
303
|
|
|
303
304
|
permission_classes = [IsAdminUser]
|
|
304
305
|
|
|
305
|
-
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _process_input_data(request: Request) -> tuple[list[str], list[str]]:
|
|
308
|
+
"""Extract and validate input data from request."""
|
|
309
|
+
data = request.data
|
|
310
|
+
learning_paths_keys = data.get("learning_paths", "").split(",")
|
|
311
|
+
emails = data.get("emails", "").split(",")
|
|
312
|
+
|
|
313
|
+
return learning_paths_keys, emails
|
|
314
|
+
|
|
315
|
+
@staticmethod
|
|
316
|
+
def _validate_learning_paths(learning_paths_keys: list[str]) -> QuerySet[LearningPath]:
|
|
317
|
+
"""Validate learning path keys and return valid ones."""
|
|
318
|
+
valid_learning_paths_keys = []
|
|
319
|
+
for key in learning_paths_keys:
|
|
320
|
+
try:
|
|
321
|
+
LearningPathKey.from_string(key)
|
|
322
|
+
valid_learning_paths_keys.append(key)
|
|
323
|
+
except InvalidKeyError:
|
|
324
|
+
logger.warning("BulkEnrollView: Invalid learning path key: %s", key)
|
|
325
|
+
|
|
326
|
+
return LearningPath.objects.filter(key__in=valid_learning_paths_keys)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def _create_audit_data(request: Request, state_transition: str) -> dict[str, str]:
|
|
330
|
+
"""Create audit data dictionary."""
|
|
331
|
+
return {
|
|
332
|
+
"enrolled_by": request.user,
|
|
333
|
+
"reason": request.data.get("reason", ""),
|
|
334
|
+
"org": request.data.get("org", ""),
|
|
335
|
+
"role": request.data.get("role", ""),
|
|
336
|
+
"state_transition": state_transition,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
def _setup_bulk_operation(self, request: Request) -> tuple[QuerySet[LearningPath], QuerySet[User], list[str]]:
|
|
340
|
+
"""Common setup for bulk operations."""
|
|
341
|
+
learning_paths_keys, emails = self._process_input_data(request)
|
|
342
|
+
learning_paths = self._validate_learning_paths(learning_paths_keys)
|
|
343
|
+
existing_users = User.objects.filter(email__in=emails)
|
|
344
|
+
|
|
345
|
+
return learning_paths, existing_users, emails
|
|
346
|
+
|
|
347
|
+
def post(self, request: Request, *args, **kwargs) -> Response:
|
|
306
348
|
"""
|
|
307
349
|
Bulk Enroll learners in Learning Paths.
|
|
308
350
|
|
|
@@ -313,11 +355,17 @@ class BulkEnrollView(APIView):
|
|
|
313
355
|
|
|
314
356
|
{
|
|
315
357
|
"learning_paths": "learning_path_1,learning_path_2",
|
|
316
|
-
"emails": "user_1@example.com,user_2@example.com"
|
|
358
|
+
"emails": "user_1@example.com,user_2@example.com",
|
|
359
|
+
"reason": "Bulk enrollment for new cohort",
|
|
360
|
+
"org": "organization_name",
|
|
361
|
+
"role": "student"
|
|
317
362
|
}
|
|
318
363
|
|
|
319
364
|
`learning_paths` (str): A comma separated list of learning path IDs.
|
|
320
365
|
`emails` (str): A comma separated list of email addresses.
|
|
366
|
+
`reason` (str, optional): Reason for enrollment, used for audit.
|
|
367
|
+
`org` (str, optional): Organization identifier, used for audit.
|
|
368
|
+
`role` (str, optional): User role, used for audit.
|
|
321
369
|
|
|
322
370
|
* For existing users, it creates a new LearningPathEnrollment record, automatically
|
|
323
371
|
enrolling them in the learning path. It also creates a LearningPathAllowed record
|
|
@@ -326,41 +374,18 @@ class BulkEnrollView(APIView):
|
|
|
326
374
|
with just the email address, allowing them to get enrolled when they register.
|
|
327
375
|
|
|
328
376
|
"""
|
|
329
|
-
|
|
330
|
-
learning_paths_keys = data.get("learning_paths", "").split(",")
|
|
331
|
-
emails = data.get("emails", "").split(",")
|
|
332
|
-
|
|
333
|
-
audit_data = {
|
|
334
|
-
"enrolled_by": request.user,
|
|
335
|
-
"reason": data.get("reason", ""),
|
|
336
|
-
"org": data.get("org", ""),
|
|
337
|
-
"role": data.get("role", ""),
|
|
338
|
-
"state_transition": LearningPathEnrollmentAudit.DEFAULT_TRANSITION_STATE,
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
valid_learning_paths_keys = []
|
|
342
|
-
for key in learning_paths_keys:
|
|
343
|
-
try:
|
|
344
|
-
LearningPathKey.from_string(key)
|
|
345
|
-
valid_learning_paths_keys.append(key)
|
|
346
|
-
except InvalidKeyError:
|
|
347
|
-
logger.warning("BulkEnrollView: Invalid learning path key: %s", key)
|
|
348
|
-
|
|
349
|
-
learning_paths = LearningPath.objects.filter(key__in=valid_learning_paths_keys)
|
|
350
|
-
|
|
351
|
-
existing_users = User.objects.filter(email__in=emails)
|
|
377
|
+
learning_paths, existing_users, emails = self._setup_bulk_operation(request)
|
|
352
378
|
non_existing_emails = set(emails) - set(u.email for u in existing_users)
|
|
353
379
|
|
|
354
380
|
enrollments_created = []
|
|
355
381
|
enrollment_allowed_created = []
|
|
356
382
|
|
|
357
383
|
for learning_path in learning_paths:
|
|
358
|
-
|
|
359
384
|
# Create LearningPathEnrollment for existing users
|
|
360
385
|
for user in existing_users:
|
|
361
386
|
enrollment = LearningPathEnrollment.objects.filter(user=user, learning_path=learning_path).first()
|
|
362
387
|
enrolled_now = False
|
|
363
|
-
audit_data
|
|
388
|
+
audit_data = self._create_audit_data(request, LearningPathEnrollmentAudit.UNENROLLED_TO_ENROLLED)
|
|
364
389
|
if enrollment:
|
|
365
390
|
if not enrollment.is_active:
|
|
366
391
|
enrollment.is_active = True
|
|
@@ -391,7 +416,7 @@ class BulkEnrollView(APIView):
|
|
|
391
416
|
if created:
|
|
392
417
|
enrollment_allowed_created.append(allowed)
|
|
393
418
|
|
|
394
|
-
audit_data
|
|
419
|
+
audit_data = self._create_audit_data(request, LearningPathEnrollmentAudit.UNENROLLED_TO_ALLOWEDTOENROLL)
|
|
395
420
|
allowed._audit = audit_data # pylint: disable=protected-access
|
|
396
421
|
allowed.save()
|
|
397
422
|
|
|
@@ -403,6 +428,59 @@ class BulkEnrollView(APIView):
|
|
|
403
428
|
status=status.HTTP_201_CREATED,
|
|
404
429
|
)
|
|
405
430
|
|
|
431
|
+
def delete(self, request, *args, **kwargs) -> Response:
|
|
432
|
+
"""
|
|
433
|
+
Bulk Unenroll learners from Learning Paths.
|
|
434
|
+
|
|
435
|
+
The "bulk unenroll" API provides a way for the staff to unenroll multiple learners
|
|
436
|
+
from multiple learning paths at once.
|
|
437
|
+
|
|
438
|
+
Example payload::
|
|
439
|
+
|
|
440
|
+
{
|
|
441
|
+
"learning_paths": "learning_path_1,learning_path_2",
|
|
442
|
+
"emails": "user_1@example.com,user_2@example.com",
|
|
443
|
+
"reason": "End of semester cleanup",
|
|
444
|
+
"org": "organization_name",
|
|
445
|
+
"role": "student"
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
`learning_paths` (str): A comma separated list of learning path IDs.
|
|
449
|
+
`emails` (str): A comma separated list of email addresses.
|
|
450
|
+
`reason` (str, optional): Reason for unenrollment, used for audit.
|
|
451
|
+
`org` (str, optional): Organization identifier, used for audit.
|
|
452
|
+
`role` (str, optional): User role, used for audit.
|
|
453
|
+
|
|
454
|
+
* For existing users, it deactivates their LearningPathEnrollment records.
|
|
455
|
+
* Does not affect LearningPathEnrollmentAllowed records (allowed to enroll records).
|
|
456
|
+
|
|
457
|
+
"""
|
|
458
|
+
learning_paths, existing_users, _ = self._setup_bulk_operation(request)
|
|
459
|
+
|
|
460
|
+
enrollments_unenrolled = []
|
|
461
|
+
|
|
462
|
+
for learning_path in learning_paths:
|
|
463
|
+
for user in existing_users:
|
|
464
|
+
enrollment = LearningPathEnrollment.objects.filter(user=user, learning_path=learning_path).first()
|
|
465
|
+
|
|
466
|
+
if enrollment:
|
|
467
|
+
if enrollment.is_active:
|
|
468
|
+
state_transition = LearningPathEnrollmentAudit.ENROLLED_TO_UNENROLLED
|
|
469
|
+
enrollment.is_active = False
|
|
470
|
+
enrollments_unenrolled.append(enrollment)
|
|
471
|
+
else:
|
|
472
|
+
state_transition = LearningPathEnrollmentAudit.UNENROLLED_TO_UNENROLLED
|
|
473
|
+
audit_data = self._create_audit_data(request, state_transition)
|
|
474
|
+
enrollment._audit = audit_data # pylint: disable=protected-access
|
|
475
|
+
enrollment.save()
|
|
476
|
+
|
|
477
|
+
return Response(
|
|
478
|
+
{
|
|
479
|
+
"enrollments_unenrolled": len(enrollments_unenrolled),
|
|
480
|
+
},
|
|
481
|
+
status=status.HTTP_204_NO_CONTENT,
|
|
482
|
+
)
|
|
483
|
+
|
|
406
484
|
|
|
407
485
|
class LearningPathCourseEnrollmentView(APIView):
|
|
408
486
|
"""API View to enroll a user in a course that's part of a learning path."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-paths-plugin
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4rc3
|
|
4
4
|
Summary: Learning Paths plugin
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-paths-plugin
|
|
6
6
|
Author: OpenCraft
|
|
@@ -126,6 +126,7 @@ Unreleased
|
|
|
126
126
|
Added
|
|
127
127
|
=====
|
|
128
128
|
|
|
129
|
+
* Bulk unenrollment API.
|
|
129
130
|
* Enrollment audit model that tracks the enrollment state transitions.
|
|
130
131
|
|
|
131
132
|
0.3.3 - 2025-05-23
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/filters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/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
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/learning_paths/receivers.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_paths_plugin-0.3.4rc2 → learning_paths_plugin-0.3.4rc3}/requirements/constraints.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|