learning-paths-plugin 0.3.4rc1__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.4rc1 → learning_paths_plugin-0.3.4rc3}/CHANGELOG.rst +1 -0
- {learning_paths_plugin-0.3.4rc1/learning_paths_plugin.egg-info → learning_paths_plugin-0.3.4rc3}/PKG-INFO +2 -1
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/__init__.py +1 -1
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/admin.py +25 -16
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/views.py +108 -30
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3/learning_paths_plugin.egg-info}/PKG-INFO +2 -1
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/LICENSE.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/MANIFEST.in +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/README.rst +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/filters.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/permissions.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/serializers.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/utils.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/apps.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/compat.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/conftest.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/keys.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0001_initial.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0002_learningpath_uuid.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0003_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0004_auto_20240207_1633.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0005_learningpathstep_weight_learningpathgradingcriteria.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0006_enrollment_models.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0007_replace_uuid_with_learningpathkey.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → 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.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0009_remove_learningpath_slug.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0010_learningpath_invite_only.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0011_replace_learningpath_image_url_with_image.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0012_alter_learningpath_subtitle.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/0013_enrollment_audit.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/migrations/__init__.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/models.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/receivers.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/settings.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/templates/learning_paths/base.html +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/urls.py +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/SOURCES.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/dependency_links.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/entry_points.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/not-zip-safe +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/requires.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths_plugin.egg-info/top_level.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/pyproject.toml +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/requirements/base.in +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/requirements/constraints.txt +0 -0
- {learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/setup.cfg +0 -0
- {learning_paths_plugin-0.3.4rc1 → 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
|
|
@@ -137,6 +137,7 @@ class BulkEnrollUsersForm(forms.ModelForm):
|
|
|
137
137
|
return users
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
@admin.register(LearningPath)
|
|
140
141
|
class LearningPathAdmin(admin.ModelAdmin):
|
|
141
142
|
"""Admin for Learning Path."""
|
|
142
143
|
|
|
@@ -178,13 +179,14 @@ class LearningPathAdmin(admin.ModelAdmin):
|
|
|
178
179
|
LearningPathEnrollment.objects.get_or_create(user=user, learning_path=form.instance)
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
@admin.register(Skill)
|
|
181
183
|
class SkillAdmin(admin.ModelAdmin):
|
|
182
184
|
"""Admin for Learning Path generic skill."""
|
|
183
185
|
|
|
184
186
|
model = Skill
|
|
185
187
|
|
|
186
188
|
|
|
187
|
-
class
|
|
189
|
+
class EnrollmentAuditInline(admin.TabularInline):
|
|
188
190
|
"""Inline admin for LearningPathEnrollmentAudit records."""
|
|
189
191
|
|
|
190
192
|
model = LearningPathEnrollmentAudit
|
|
@@ -209,7 +211,7 @@ class LearningPathEnrollmentAuditInline(admin.TabularInline):
|
|
|
209
211
|
return False
|
|
210
212
|
|
|
211
213
|
|
|
212
|
-
class
|
|
214
|
+
class EnrollmentAllowedAuditInline(admin.TabularInline):
|
|
213
215
|
"""Inline admin for LearningPathEnrollmentAudit records related to enrollment allowed."""
|
|
214
216
|
|
|
215
217
|
model = LearningPathEnrollmentAudit
|
|
@@ -234,13 +236,28 @@ class LearningPathEnrollmentAllowedAuditInline(admin.TabularInline):
|
|
|
234
236
|
return False
|
|
235
237
|
|
|
236
238
|
|
|
239
|
+
@admin.register(LearningPathEnrollment)
|
|
237
240
|
class EnrolledUsersAdmin(admin.ModelAdmin):
|
|
238
241
|
"""Admin for Learning Path enrollment."""
|
|
239
242
|
|
|
240
243
|
model = LearningPathEnrollment
|
|
241
244
|
raw_id_fields = ("user",)
|
|
242
245
|
autocomplete_fields = ["learning_path"]
|
|
243
|
-
inlines = [
|
|
246
|
+
inlines = [EnrollmentAuditInline]
|
|
247
|
+
|
|
248
|
+
list_display = [
|
|
249
|
+
"id",
|
|
250
|
+
"user",
|
|
251
|
+
"learning_path",
|
|
252
|
+
"enrolled_at",
|
|
253
|
+
"is_active",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
list_filter = [
|
|
257
|
+
"learning_path__key",
|
|
258
|
+
"created",
|
|
259
|
+
"is_active",
|
|
260
|
+
]
|
|
244
261
|
|
|
245
262
|
search_fields = [
|
|
246
263
|
"id",
|
|
@@ -251,7 +268,7 @@ class EnrolledUsersAdmin(admin.ModelAdmin):
|
|
|
251
268
|
|
|
252
269
|
|
|
253
270
|
@admin.register(LearningPathEnrollmentAllowed)
|
|
254
|
-
class
|
|
271
|
+
class EnrollmentAllowedAdmin(admin.ModelAdmin):
|
|
255
272
|
"""Admin configuration for LearningPathEnrollmentAllowed model."""
|
|
256
273
|
|
|
257
274
|
list_display = [
|
|
@@ -264,7 +281,6 @@ class LearningPathEnrollmentAllowedAdmin(admin.ModelAdmin):
|
|
|
264
281
|
|
|
265
282
|
list_filter = [
|
|
266
283
|
"learning_path",
|
|
267
|
-
"user",
|
|
268
284
|
"created",
|
|
269
285
|
]
|
|
270
286
|
|
|
@@ -272,7 +288,6 @@ class LearningPathEnrollmentAllowedAdmin(admin.ModelAdmin):
|
|
|
272
288
|
"email",
|
|
273
289
|
"user__username",
|
|
274
290
|
"user__email",
|
|
275
|
-
"learning_path__title",
|
|
276
291
|
"learning_path__key",
|
|
277
292
|
]
|
|
278
293
|
|
|
@@ -282,7 +297,7 @@ class LearningPathEnrollmentAllowedAdmin(admin.ModelAdmin):
|
|
|
282
297
|
"modified",
|
|
283
298
|
]
|
|
284
299
|
|
|
285
|
-
inlines = [
|
|
300
|
+
inlines = [EnrollmentAllowedAuditInline]
|
|
286
301
|
|
|
287
302
|
def get_user(self, obj):
|
|
288
303
|
"""Get the associated user, if any."""
|
|
@@ -292,7 +307,7 @@ class LearningPathEnrollmentAllowedAdmin(admin.ModelAdmin):
|
|
|
292
307
|
|
|
293
308
|
|
|
294
309
|
@admin.register(LearningPathEnrollmentAudit)
|
|
295
|
-
class
|
|
310
|
+
class EnrollmentAuditAdmin(admin.ModelAdmin):
|
|
296
311
|
"""Admin configuration for LearningPathEnrollmentAudit model."""
|
|
297
312
|
|
|
298
313
|
list_display = [
|
|
@@ -311,7 +326,6 @@ class LearningPathEnrollmentAuditAdmin(admin.ModelAdmin):
|
|
|
311
326
|
"created",
|
|
312
327
|
"org",
|
|
313
328
|
"role",
|
|
314
|
-
"enrolled_by",
|
|
315
329
|
]
|
|
316
330
|
|
|
317
331
|
search_fields = [
|
|
@@ -320,8 +334,8 @@ class LearningPathEnrollmentAuditAdmin(admin.ModelAdmin):
|
|
|
320
334
|
"enrollment__user__username",
|
|
321
335
|
"enrollment__user__email",
|
|
322
336
|
"enrollment_allowed__email",
|
|
323
|
-
"
|
|
324
|
-
"
|
|
337
|
+
"enrollment__learning_path__key",
|
|
338
|
+
"enrollment_allowed__learning_path__key",
|
|
325
339
|
"reason",
|
|
326
340
|
]
|
|
327
341
|
|
|
@@ -353,8 +367,3 @@ class LearningPathEnrollmentAuditAdmin(admin.ModelAdmin):
|
|
|
353
367
|
return "-"
|
|
354
368
|
|
|
355
369
|
get_learning_path.short_description = "Learning Path"
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
admin.site.register(LearningPath, LearningPathAdmin)
|
|
359
|
-
admin.site.register(Skill, SkillAdmin)
|
|
360
|
-
admin.site.register(LearningPathEnrollment, EnrolledUsersAdmin)
|
{learning_paths_plugin-0.3.4rc1 → 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
|
{learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/__init__.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc1 → 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.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/api/v1/urls.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc1 → 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.4rc1 → 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.4rc1 → learning_paths_plugin-0.3.4rc3}/learning_paths/receivers.py
RENAMED
|
File without changes
|
{learning_paths_plugin-0.3.4rc1 → 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.4rc1 → learning_paths_plugin-0.3.4rc3}/requirements/constraints.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|