learning-credentials 0.3.0rc3__py3-none-any.whl → 0.3.0rc10__py3-none-any.whl
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_credentials/api/v1/permissions.py +42 -24
- learning_credentials/api/v1/urls.py +4 -8
- learning_credentials/api/v1/views.py +28 -302
- learning_credentials/apps.py +7 -10
- learning_credentials/compat.py +1 -7
- learning_credentials/generators.py +0 -1
- learning_credentials/models.py +10 -44
- learning_credentials/processors.py +40 -148
- learning_credentials/settings/common.py +5 -2
- learning_credentials/settings/production.py +5 -2
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/METADATA +19 -3
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/RECORD +16 -22
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/entry_points.txt +0 -3
- learning_credentials/api/v1/serializers.py +0 -74
- learning_credentials/core_api.py +0 -77
- learning_credentials/public/css/credentials_xblock.css +0 -7
- learning_credentials/public/html/credentials_xblock.html +0 -48
- learning_credentials/public/js/credentials_xblock.js +0 -23
- learning_credentials/xblocks.py +0 -85
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/WHEEL +0 -0
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/licenses/LICENSE.txt +0 -0
- {learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/top_level.txt +0 -0
|
@@ -38,63 +38,46 @@ log = logging.getLogger(__name__)
|
|
|
38
38
|
|
|
39
39
|
def _process_learning_context(
|
|
40
40
|
learning_context_key: LearningContextKey,
|
|
41
|
-
course_processor: Callable[[CourseKey, dict[str, Any]
|
|
41
|
+
course_processor: Callable[[CourseKey, dict[str, Any]], list[int]],
|
|
42
42
|
options: dict[str, Any],
|
|
43
|
-
|
|
44
|
-
) -> dict[int, dict[str, Any]]:
|
|
43
|
+
) -> list[int]:
|
|
45
44
|
"""
|
|
46
45
|
Process a learning context (course or learning path) using the given course processor function.
|
|
47
46
|
|
|
48
47
|
For courses, runs the processor directly. For learning paths, runs the processor on each
|
|
49
|
-
course in the path with step-specific options (if available), and returns
|
|
50
|
-
|
|
48
|
+
course in the path with step-specific options (if available), and returns the intersection
|
|
49
|
+
of eligible users across all courses.
|
|
51
50
|
|
|
52
51
|
Args:
|
|
53
52
|
learning_context_key: A course key or learning path key to process
|
|
54
|
-
course_processor: A function that processes a single course and returns
|
|
53
|
+
course_processor: A function that processes a single course and returns eligible user IDs
|
|
55
54
|
options: Options to pass to the processor. For learning paths, may contain a "steps" key
|
|
56
55
|
with step-specific options in the format: {"steps": {"<course_key>": {...}}}
|
|
57
|
-
user_id: Optional. If provided, will filter to the specific user.
|
|
58
56
|
|
|
59
57
|
Returns:
|
|
60
|
-
A
|
|
58
|
+
A list of eligible user IDs
|
|
61
59
|
"""
|
|
62
60
|
if learning_context_key.is_course:
|
|
63
|
-
return course_processor(learning_context_key, options
|
|
61
|
+
return course_processor(learning_context_key, options)
|
|
64
62
|
|
|
65
63
|
learning_path = LearningPath.objects.get(key=learning_context_key)
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
results = None
|
|
66
|
+
for course in learning_path.steps.all():
|
|
67
|
+
course_options = options.get("steps", {}).get(str(course.course_key), options)
|
|
68
|
+
course_results = set(course_processor(course.course_key, course_options))
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
step_results_by_course[str(step.course_key)] = step_results
|
|
76
|
-
all_user_ids.update(step_results.keys())
|
|
70
|
+
if results is None:
|
|
71
|
+
results = course_results
|
|
72
|
+
else:
|
|
73
|
+
results &= course_results
|
|
77
74
|
|
|
78
75
|
# Filter out users who are not enrolled in the Learning Path.
|
|
79
|
-
|
|
80
|
-
learning_path.enrolled_users.filter(learningpathenrollment__is_active=True).values_list('id', flat=True)
|
|
76
|
+
results &= set(
|
|
77
|
+
learning_path.enrolled_users.filter(learningpathenrollment__is_active=True).values_list('id', flat=True),
|
|
81
78
|
)
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
for uid in all_user_ids:
|
|
85
|
-
overall_eligible = True
|
|
86
|
-
user_step_results = {}
|
|
87
|
-
|
|
88
|
-
for course_key, step_results in step_results_by_course.items():
|
|
89
|
-
if uid in step_results:
|
|
90
|
-
user_step_results[course_key] = step_results[uid]
|
|
91
|
-
overall_eligible = overall_eligible and step_results[uid].get('is_eligible', False)
|
|
92
|
-
else:
|
|
93
|
-
overall_eligible = False
|
|
94
|
-
|
|
95
|
-
final_results[uid] = {'is_eligible': overall_eligible, 'steps': user_step_results}
|
|
96
|
-
|
|
97
|
-
return final_results
|
|
80
|
+
return list(results) if results else []
|
|
98
81
|
|
|
99
82
|
|
|
100
83
|
def _get_category_weights(course_id: CourseKey) -> dict[str, float]:
|
|
@@ -117,7 +100,7 @@ def _get_category_weights(course_id: CourseKey) -> dict[str, float]:
|
|
|
117
100
|
return category_weight_ratios
|
|
118
101
|
|
|
119
102
|
|
|
120
|
-
def _get_grades_by_format(course_id: CourseKey, users: list[User]) -> dict[int, dict[str,
|
|
103
|
+
def _get_grades_by_format(course_id: CourseKey, users: list[User]) -> dict[int, dict[str, int]]:
|
|
121
104
|
"""
|
|
122
105
|
Get the grades for each user, categorized by assignment types.
|
|
123
106
|
|
|
@@ -179,65 +162,30 @@ def _are_grades_passing_criteria(
|
|
|
179
162
|
return total_score >= required_grades.get('total', 0)
|
|
180
163
|
|
|
181
164
|
|
|
182
|
-
def
|
|
183
|
-
|
|
184
|
-
required_grades: dict[str, float],
|
|
185
|
-
category_weights: dict[str, float],
|
|
186
|
-
) -> dict[str, Any]:
|
|
187
|
-
"""
|
|
188
|
-
Calculate detailed progress information for grade-based criteria.
|
|
189
|
-
|
|
190
|
-
:param user_grades: The grades of the user, divided by category.
|
|
191
|
-
:param required_grades: The required grades for each category.
|
|
192
|
-
:param category_weights: The weight of each category.
|
|
193
|
-
:returns: Dict with is_eligible, current_grades, and required_grades.
|
|
194
|
-
"""
|
|
195
|
-
# Calculate total score
|
|
196
|
-
total_score = 0
|
|
197
|
-
for category, score in user_grades.items():
|
|
198
|
-
if category in category_weights:
|
|
199
|
-
total_score += score * category_weights[category]
|
|
200
|
-
|
|
201
|
-
# Add total to user grades
|
|
202
|
-
user_grades_with_total = {**user_grades, 'total': total_score}
|
|
203
|
-
|
|
204
|
-
is_eligible = _are_grades_passing_criteria(user_grades, required_grades, category_weights)
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
'is_eligible': is_eligible,
|
|
208
|
-
'current_grades': user_grades_with_total,
|
|
209
|
-
'required_grades': required_grades,
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def _retrieve_course_subsection_grades(
|
|
214
|
-
course_id: CourseKey, options: dict[str, Any], user_id: int | None = None
|
|
215
|
-
) -> dict[int, dict[str, Any]]:
|
|
216
|
-
"""Implementation for retrieving course grades. Always returns detailed progress for all users."""
|
|
165
|
+
def _retrieve_course_subsection_grades(course_id: CourseKey, options: dict[str, Any]) -> list[int]:
|
|
166
|
+
"""Implementation for retrieving course grades."""
|
|
217
167
|
required_grades: dict[str, int] = options['required_grades']
|
|
218
168
|
required_grades = {key.lower(): value * 100 for key, value in required_grades.items()}
|
|
219
169
|
|
|
220
|
-
users = get_course_enrollments(course_id
|
|
170
|
+
users = get_course_enrollments(course_id)
|
|
221
171
|
grades = _get_grades_by_format(course_id, users)
|
|
222
172
|
log.debug(grades)
|
|
223
173
|
weights = _get_category_weights(course_id)
|
|
224
174
|
|
|
225
|
-
|
|
226
|
-
for
|
|
227
|
-
|
|
175
|
+
eligible_users = []
|
|
176
|
+
for user_id, user_grades in grades.items():
|
|
177
|
+
if _are_grades_passing_criteria(user_grades, required_grades, weights):
|
|
178
|
+
eligible_users.append(user_id)
|
|
228
179
|
|
|
229
|
-
return
|
|
180
|
+
return eligible_users
|
|
230
181
|
|
|
231
182
|
|
|
232
|
-
def retrieve_subsection_grades(
|
|
233
|
-
learning_context_key: LearningContextKey, options: dict[str, Any], user_id: int | None = None
|
|
234
|
-
) -> list[int] | dict[str, Any]:
|
|
183
|
+
def retrieve_subsection_grades(learning_context_key: LearningContextKey, options: dict[str, Any]) -> list[int]:
|
|
235
184
|
"""
|
|
236
185
|
Retrieve the users that have passing grades in all required categories.
|
|
237
186
|
|
|
238
187
|
:param learning_context_key: The learning context key (course or learning path).
|
|
239
188
|
:param options: The custom options for the credential.
|
|
240
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
241
189
|
:returns: The IDs of the users that have passing grades in all required categories.
|
|
242
190
|
|
|
243
191
|
Options:
|
|
@@ -284,16 +232,7 @@ def retrieve_subsection_grades(
|
|
|
284
232
|
}
|
|
285
233
|
}
|
|
286
234
|
"""
|
|
287
|
-
|
|
288
|
-
learning_context_key, _retrieve_course_subsection_grades, options, user_id
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
if user_id is not None:
|
|
292
|
-
if user_id in detailed_results:
|
|
293
|
-
return detailed_results[user_id]
|
|
294
|
-
return {'is_eligible': False, 'current_grades': {}, 'required_grades': {}}
|
|
295
|
-
|
|
296
|
-
return [uid for uid, result in detailed_results.items() if result.get('is_eligible', False)]
|
|
235
|
+
return _process_learning_context(learning_context_key, _retrieve_course_subsection_grades, options)
|
|
297
236
|
|
|
298
237
|
|
|
299
238
|
def _prepare_request_to_completion_aggregator(course_id: CourseKey, query_params: dict, url: str) -> APIView:
|
|
@@ -313,7 +252,7 @@ def _prepare_request_to_completion_aggregator(course_id: CourseKey, query_params
|
|
|
313
252
|
drf_request = Request(django_request) # convert django.core.handlers.wsgi.WSGIRequest to DRF request
|
|
314
253
|
|
|
315
254
|
view = CompletionDetailView()
|
|
316
|
-
view.request = drf_request
|
|
255
|
+
view.request = drf_request
|
|
317
256
|
|
|
318
257
|
# HACK: Bypass the API permissions.
|
|
319
258
|
staff_user = get_user_model().objects.filter(is_staff=True).first()
|
|
@@ -323,10 +262,8 @@ def _prepare_request_to_completion_aggregator(course_id: CourseKey, query_params
|
|
|
323
262
|
return view
|
|
324
263
|
|
|
325
264
|
|
|
326
|
-
def _retrieve_course_completions(
|
|
327
|
-
|
|
328
|
-
) -> dict[int, dict[str, Any]]:
|
|
329
|
-
"""Implementation for retrieving course completions. Always returns detailed progress for all users."""
|
|
265
|
+
def _retrieve_course_completions(course_id: CourseKey, options: dict[str, Any]) -> list[int]:
|
|
266
|
+
"""Implementation for retrieving course completions."""
|
|
330
267
|
# If it turns out to be too slow, we can:
|
|
331
268
|
# 1. Modify the Completion Aggregator to emit a signal/event when a user achieves a certain completion threshold.
|
|
332
269
|
# 2. Get this data from the `Aggregator` model. Filter by `aggregation name == 'course'`, `course_key`, `percent`.
|
|
@@ -339,47 +276,29 @@ def _retrieve_course_completions(
|
|
|
339
276
|
|
|
340
277
|
# TODO: Extract the logic of this view into an API. The current approach is very hacky.
|
|
341
278
|
view = _prepare_request_to_completion_aggregator(course_id, query_params.copy(), url)
|
|
342
|
-
completions =
|
|
279
|
+
completions = []
|
|
343
280
|
|
|
344
281
|
while True:
|
|
345
282
|
# noinspection PyUnresolvedReferences
|
|
346
283
|
response = view.get(view.request, str(course_id))
|
|
347
284
|
log.debug(response.data)
|
|
348
|
-
|
|
349
|
-
|
|
285
|
+
completions.extend(
|
|
286
|
+
res['username'] for res in response.data['results'] if res['completion']['percent'] >= required_completion
|
|
287
|
+
)
|
|
350
288
|
if not response.data['pagination']['next']:
|
|
351
289
|
break
|
|
352
290
|
query_params['page'] += 1
|
|
353
291
|
view = _prepare_request_to_completion_aggregator(course_id, query_params.copy(), url)
|
|
354
292
|
|
|
355
|
-
|
|
356
|
-
users = get_course_enrollments(course_id, user_id)
|
|
357
|
-
username_to_id = {user.username: user.id for user in users} # type: ignore[unresolved-attribute]
|
|
358
|
-
|
|
359
|
-
# Always return detailed progress for all users as dict
|
|
360
|
-
detailed_results = {}
|
|
361
|
-
for username, current_completion in completions.items():
|
|
362
|
-
if username in username_to_id:
|
|
363
|
-
user_id_for_result = username_to_id[username]
|
|
364
|
-
progress = {
|
|
365
|
-
'is_eligible': current_completion >= required_completion,
|
|
366
|
-
'current_completion': current_completion,
|
|
367
|
-
'required_completion': required_completion,
|
|
368
|
-
}
|
|
369
|
-
detailed_results[user_id_for_result] = progress
|
|
293
|
+
return list(get_user_model().objects.filter(username__in=completions).values_list('id', flat=True))
|
|
370
294
|
|
|
371
|
-
return detailed_results
|
|
372
295
|
|
|
373
|
-
|
|
374
|
-
def retrieve_completions(
|
|
375
|
-
learning_context_key: LearningContextKey, options: dict[str, Any], user_id: int | None = None
|
|
376
|
-
) -> list[int] | dict[str, Any]:
|
|
296
|
+
def retrieve_completions(learning_context_key: LearningContextKey, options: dict[str, Any]) -> list[int]:
|
|
377
297
|
"""
|
|
378
298
|
Retrieve the course completions for all users through the Completion Aggregator API.
|
|
379
299
|
|
|
380
300
|
:param learning_context_key: The learning context key (course or learning path).
|
|
381
301
|
:param options: The custom options for the credential.
|
|
382
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
383
302
|
:returns: The IDs of the users that have achieved the required completion percentage.
|
|
384
303
|
|
|
385
304
|
Options:
|
|
@@ -400,19 +319,10 @@ def retrieve_completions(
|
|
|
400
319
|
}
|
|
401
320
|
}
|
|
402
321
|
"""
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if user_id is not None:
|
|
406
|
-
if user_id in detailed_results:
|
|
407
|
-
return detailed_results[user_id]
|
|
408
|
-
return {'is_eligible': False, 'current_completion': 0.0, 'required_completion': 0.9}
|
|
322
|
+
return _process_learning_context(learning_context_key, _retrieve_course_completions, options)
|
|
409
323
|
|
|
410
|
-
return [uid for uid, result in detailed_results.items() if result.get('is_eligible', False)]
|
|
411
324
|
|
|
412
|
-
|
|
413
|
-
def retrieve_completions_and_grades(
|
|
414
|
-
learning_context_key: LearningContextKey, options: dict[str, Any], user_id: int | None = None
|
|
415
|
-
) -> list[int] | dict[str, Any]:
|
|
325
|
+
def retrieve_completions_and_grades(learning_context_key: LearningContextKey, options: dict[str, Any]) -> list[int]:
|
|
416
326
|
"""
|
|
417
327
|
Retrieve the users that meet both completion and grade criteria.
|
|
418
328
|
|
|
@@ -421,7 +331,6 @@ def retrieve_completions_and_grades(
|
|
|
421
331
|
|
|
422
332
|
:param learning_context_key: The learning context key (course or learning path).
|
|
423
333
|
:param options: The custom options for the credential.
|
|
424
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
425
334
|
:returns: The IDs of the users that meet both sets of criteria.
|
|
426
335
|
|
|
427
336
|
Options:
|
|
@@ -463,23 +372,6 @@ def retrieve_completions_and_grades(
|
|
|
463
372
|
}
|
|
464
373
|
}
|
|
465
374
|
"""
|
|
466
|
-
if user_id is not None:
|
|
467
|
-
completion_result = retrieve_completions(learning_context_key, options, user_id)
|
|
468
|
-
grades_result = retrieve_subsection_grades(learning_context_key, options, user_id)
|
|
469
|
-
|
|
470
|
-
if type(grades_result) is not dict or type(completion_result) is not dict:
|
|
471
|
-
msg = 'Both results must be dictionaries when user_id is provided.'
|
|
472
|
-
raise ValueError(msg)
|
|
473
|
-
|
|
474
|
-
completion_eligible = completion_result.get('is_eligible', False)
|
|
475
|
-
grades_eligible = grades_result.get('is_eligible', False)
|
|
476
|
-
|
|
477
|
-
return {
|
|
478
|
-
**completion_result,
|
|
479
|
-
**grades_result,
|
|
480
|
-
'is_eligible': completion_eligible and grades_eligible,
|
|
481
|
-
}
|
|
482
|
-
|
|
483
375
|
completion_eligible_users = set(retrieve_completions(learning_context_key, options))
|
|
484
376
|
grades_eligible_users = set(retrieve_subsection_grades(learning_context_key, options))
|
|
485
377
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""App-specific settings for all environments."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from django.conf import Settings
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
def plugin_settings(settings: 'Settings'):
|
|
7
10
|
"""Add `django_celery_beat` to `INSTALLED_APPS`."""
|
|
8
11
|
if 'django_celery_beat' not in settings.INSTALLED_APPS:
|
|
9
12
|
settings.INSTALLED_APPS += ('django_celery_beat',)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"""App-specific settings for production environments."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from django.conf import Settings
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
def plugin_settings(settings: 'Settings'):
|
|
7
10
|
"""
|
|
8
11
|
Use the database scheduler for Celery Beat.
|
|
9
12
|
|
{learning_credentials-0.3.0rc3.dist-info → learning_credentials-0.3.0rc10.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.0rc10
|
|
4
4
|
Summary: A pluggable service for preparing Open edX credentials.
|
|
5
5
|
Author-email: OpenCraft <help@opencraft.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -21,6 +21,8 @@ Description-Content-Type: text/x-rst
|
|
|
21
21
|
License-File: LICENSE.txt
|
|
22
22
|
Requires-Dist: django
|
|
23
23
|
Requires-Dist: django-model-utils
|
|
24
|
+
Requires-Dist: edx-api-doc-tools
|
|
25
|
+
Requires-Dist: edx-django-utils
|
|
24
26
|
Requires-Dist: edx-opaque-keys
|
|
25
27
|
Requires-Dist: celery
|
|
26
28
|
Requires-Dist: django-celery-beat
|
|
@@ -31,7 +33,6 @@ Requires-Dist: pypdf
|
|
|
31
33
|
Requires-Dist: reportlab
|
|
32
34
|
Requires-Dist: openedx-completion-aggregator
|
|
33
35
|
Requires-Dist: edx_ace
|
|
34
|
-
Requires-Dist: edx-api-doc-tools
|
|
35
36
|
Requires-Dist: learning-paths-plugin>=0.3.4
|
|
36
37
|
Dynamic: license-file
|
|
37
38
|
|
|
@@ -123,7 +124,7 @@ file in this repo.
|
|
|
123
124
|
Reporting Security Issues
|
|
124
125
|
*************************
|
|
125
126
|
|
|
126
|
-
Please do not report security issues in public. Please email
|
|
127
|
+
Please do not report security issues in public. Please email help@opencraft.com.
|
|
127
128
|
|
|
128
129
|
.. |pypi-badge| image:: https://img.shields.io/pypi/v/learning-credentials.svg
|
|
129
130
|
:target: https://pypi.python.org/pypi/learning-credentials/
|
|
@@ -175,6 +176,21 @@ Unreleased
|
|
|
175
176
|
|
|
176
177
|
*
|
|
177
178
|
|
|
179
|
+
0.3.0 - 2025-09-17
|
|
180
|
+
******************
|
|
181
|
+
|
|
182
|
+
Added
|
|
183
|
+
=====
|
|
184
|
+
|
|
185
|
+
* REST API endpoint to check if credentials are configured for a learning context.
|
|
186
|
+
|
|
187
|
+
0.2.4 - 2025-09-07
|
|
188
|
+
|
|
189
|
+
Added
|
|
190
|
+
=====
|
|
191
|
+
|
|
192
|
+
* Option to customize the learner's name size on the PDF certificate.
|
|
193
|
+
|
|
178
194
|
0.2.3 - 2025-08-18
|
|
179
195
|
|
|
180
196
|
Modified
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
learning_credentials/__init__.py,sha256=8Q0-3Hdnfmcj41EKu1GSfzEfwWcYNDlItyEEke2r9bs,62
|
|
2
2
|
learning_credentials/admin.py,sha256=ynK3tVJwLsIeV7Jk66t1FAVyVsU1G-KRIAdRkycVTmA,10439
|
|
3
|
-
learning_credentials/apps.py,sha256=
|
|
4
|
-
learning_credentials/compat.py,sha256=
|
|
5
|
-
learning_credentials/core_api.py,sha256=ZVNPm7SQk54roZ8rmZ0Bc6T6NLvumKWJZjJepAxM6L8,3232
|
|
3
|
+
learning_credentials/apps.py,sha256=7lHAqhBdOMWknA_muPxqC1RX9_8CiOMeQ79ioKA3MlY,1073
|
|
4
|
+
learning_credentials/compat.py,sha256=OvgzxnhGG5A7Ij65mBv3kyaYojKKQ381RbUGsWXtyOg,4651
|
|
6
5
|
learning_credentials/exceptions.py,sha256=UaqBVXFMWR2Iob7_LMb3j4NNVmWQFAgLi_MNMRUvGsI,290
|
|
7
|
-
learning_credentials/generators.py,sha256=
|
|
8
|
-
learning_credentials/models.py,sha256=
|
|
9
|
-
learning_credentials/processors.py,sha256=
|
|
6
|
+
learning_credentials/generators.py,sha256=KCB166rOx-bwnm6kOc-Vz2HomGdJrQXqglv2MPVBF_Q,9075
|
|
7
|
+
learning_credentials/models.py,sha256=Wepzng9WYDAxF8ptyQokp_9jCmuEv_4FY7ytkKFS4uU,16047
|
|
8
|
+
learning_credentials/processors.py,sha256=LkdjmkLBnXc9qeMcksB1T8AQ5ZhYaECyQO__KfHB_aU,15212
|
|
10
9
|
learning_credentials/tasks.py,sha256=byoFEUvN_ayVaU5K5SlEiA7vu9BRPaSSmKnB9g5toec,1927
|
|
11
10
|
learning_credentials/urls.py,sha256=9Xc-imliMCIOWqFHfm-CSAgwm2tQGfMR18jCyBpKcho,176
|
|
12
|
-
learning_credentials/xblocks.py,sha256=gb-bfkOlRnBzwUg7CTqBJfg-BOJ1UOtaBQRmbyGuD0w,3568
|
|
13
11
|
learning_credentials/api/__init__.py,sha256=q8sLFfwo5RwQu8FY6BJUL_Jrt3TUojbZK-Zlw9v08EM,40
|
|
14
12
|
learning_credentials/api/urls.py,sha256=JfGSbzvC5d7s9dRq4C0d-AzTDuOVnen3wvFYSJQoEdQ,255
|
|
15
13
|
learning_credentials/api/v1/__init__.py,sha256=A7ZqENtM4QM1A7j_cAfnzw4zn0kuyfXSWtylFIE0_f8,43
|
|
16
|
-
learning_credentials/api/v1/permissions.py,sha256=
|
|
17
|
-
learning_credentials/api/v1/
|
|
18
|
-
learning_credentials/api/v1/
|
|
19
|
-
learning_credentials/api/v1/views.py,sha256=LrlHRzURWi_zEii_zq55CH05tpC2ZqPhZLyebx1Jy8M,13606
|
|
14
|
+
learning_credentials/api/v1/permissions.py,sha256=TqM50TpR3JGUgZgIgKZF0-R_g1_P2V9bqKzYXgk-VvY,3436
|
|
15
|
+
learning_credentials/api/v1/urls.py,sha256=xUjG3dlbhFlQ1SM45x9tltuti-DwazX51anIDgtBo4Q,287
|
|
16
|
+
learning_credentials/api/v1/views.py,sha256=rkdj1AfRBDzrpRC5uGMAxTUf4P1zs-MSF9lpNMIgYLw,3005
|
|
20
17
|
learning_credentials/conf/locale/config.yaml,sha256=jPen2DmckNDKK30axCKEd2Q2ha9oOG3IBxrJ63Pvznk,2280
|
|
21
18
|
learning_credentials/migrations/0001_initial.py,sha256=61EvThCv-0UAnhCE5feyQVfjRodbp-6cDaAr4CY5PMA,8435
|
|
22
19
|
learning_credentials/migrations/0002_migrate_to_learning_credentials.py,sha256=vUhcnQKDdwOsppkXsjz2zZwOGMwIJ-fkQRsaj-K7l1o,1779
|
|
@@ -25,21 +22,18 @@ learning_credentials/migrations/0004_replace_course_keys_with_learning_context_k
|
|
|
25
22
|
learning_credentials/migrations/0005_rename_processors_and_generators.py,sha256=5UCqjq-CBJnRo1qBAoWs91ngyEuSMN8_tQtfzsuR5SI,5271
|
|
26
23
|
learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py,sha256=aJs_gOP4TmW9J-Dmr21m94jBfLQxzjAu6-ua7x4uYLE,727
|
|
27
24
|
learning_credentials/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
learning_credentials/public/css/credentials_xblock.css,sha256=O_16evZyQ6c1vr5IfkKVtwt9JbE1R4w7_mBaiXgfi4k,140
|
|
29
|
-
learning_credentials/public/html/credentials_xblock.html,sha256=U00jJKJfxhfKSJDcj6xyddXqjhCgi6ftCb5-2qb8qUM,2775
|
|
30
|
-
learning_credentials/public/js/credentials_xblock.js,sha256=zx0mXAPY4UOZeWKsm4rmD3yBlSDvFoX8acEoIFqKhG8,1094
|
|
31
25
|
learning_credentials/settings/__init__.py,sha256=tofc5eg3Q2lV13Ff_jjg1ggGgWpKYoeESkP1qxl3H_A,29
|
|
32
|
-
learning_credentials/settings/common.py,sha256=
|
|
33
|
-
learning_credentials/settings/production.py,sha256=
|
|
26
|
+
learning_credentials/settings/common.py,sha256=Cck-nyFt11G1NLiz-bHfKJp8MV6sDZGqTwdbC8_1WE0,360
|
|
27
|
+
learning_credentials/settings/production.py,sha256=6P0P7JxbpWNsk4Lk8lfyxHirOWMgU4UWOb3EYKLjiVQ,542
|
|
34
28
|
learning_credentials/templates/learning_credentials/base.html,sha256=wtjBYqfHmOnyEY5tN3VGOmzYLsOD24MXdEUhTZ7OmwI,662
|
|
35
29
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html,sha256=t-i1Ra9AC4pX-rPRifDJIvBBZuxCxdrFqg1NKTjHBOk,813
|
|
36
30
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt,sha256=IF_x8aF_-dORlQB-RCh0IkJDl2ktD489E8qGgLe9M3Y,677
|
|
37
31
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt,sha256=-n8tjPSwfwAfeOSZ1WhcCTrpOah4VswzMZ5mh63Pxow,20
|
|
38
32
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
33
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt,sha256=S7Hc5T_sZSsSBXm5_H5HBNNv16Ohl0oZn0nVqqeWL0g,132
|
|
40
|
-
learning_credentials-0.3.
|
|
41
|
-
learning_credentials-0.3.
|
|
42
|
-
learning_credentials-0.3.
|
|
43
|
-
learning_credentials-0.3.
|
|
44
|
-
learning_credentials-0.3.
|
|
45
|
-
learning_credentials-0.3.
|
|
34
|
+
learning_credentials-0.3.0rc10.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
|
|
35
|
+
learning_credentials-0.3.0rc10.dist-info/METADATA,sha256=0Ongz2Z3HBDYIqt6FcGEIrVTPlAljq_uaKNZ6mDgx2g,7135
|
|
36
|
+
learning_credentials-0.3.0rc10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
37
|
+
learning_credentials-0.3.0rc10.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
|
|
38
|
+
learning_credentials-0.3.0rc10.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
|
|
39
|
+
learning_credentials-0.3.0rc10.dist-info/RECORD,,
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""Serializers for the Learning Credentials API."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, ClassVar
|
|
4
|
-
|
|
5
|
-
from rest_framework import serializers
|
|
6
|
-
|
|
7
|
-
from learning_credentials.models import Credential
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CredentialModelSerializer(serializers.ModelSerializer):
|
|
11
|
-
"""Model serializer for Credential instances."""
|
|
12
|
-
|
|
13
|
-
credential_id = serializers.UUIDField(source='uuid', read_only=True)
|
|
14
|
-
credential_type = serializers.CharField(read_only=True)
|
|
15
|
-
context_key = serializers.CharField(source='learning_context_key', read_only=True)
|
|
16
|
-
created_date = serializers.DateTimeField(source='created', read_only=True)
|
|
17
|
-
download_url = serializers.URLField(read_only=True)
|
|
18
|
-
|
|
19
|
-
class Meta:
|
|
20
|
-
"""Meta configuration for CredentialModelSerializer."""
|
|
21
|
-
|
|
22
|
-
model = Credential
|
|
23
|
-
fields: ClassVar[list[str]] = [
|
|
24
|
-
'credential_id',
|
|
25
|
-
'credential_type',
|
|
26
|
-
'context_key',
|
|
27
|
-
'status',
|
|
28
|
-
'created_date',
|
|
29
|
-
'download_url',
|
|
30
|
-
]
|
|
31
|
-
read_only_fields: ClassVar[list[str]] = [
|
|
32
|
-
'credential_id',
|
|
33
|
-
'credential_type',
|
|
34
|
-
'context_key',
|
|
35
|
-
'status',
|
|
36
|
-
'created_date',
|
|
37
|
-
'download_url',
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class CredentialEligibilitySerializer(serializers.Serializer):
|
|
42
|
-
"""Serializer for credential eligibility information with dynamic fields."""
|
|
43
|
-
|
|
44
|
-
credential_type_id = serializers.IntegerField()
|
|
45
|
-
name = serializers.CharField()
|
|
46
|
-
is_eligible = serializers.BooleanField()
|
|
47
|
-
existing_credential = serializers.UUIDField(required=False, allow_null=True)
|
|
48
|
-
existing_credential_url = serializers.URLField(required=False, allow_null=True)
|
|
49
|
-
|
|
50
|
-
current_grades = serializers.DictField(required=False)
|
|
51
|
-
required_grades = serializers.DictField(required=False)
|
|
52
|
-
|
|
53
|
-
current_completion = serializers.FloatField(required=False, allow_null=True)
|
|
54
|
-
required_completion = serializers.FloatField(required=False, allow_null=True)
|
|
55
|
-
|
|
56
|
-
steps = serializers.DictField(required=False)
|
|
57
|
-
|
|
58
|
-
def to_representation(self, instance: dict) -> dict[str, Any]:
|
|
59
|
-
"""Remove null/empty fields from representation."""
|
|
60
|
-
data = super().to_representation(instance)
|
|
61
|
-
return {key: value for key, value in data.items() if value is not None and value not in ({}, [])}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class CredentialEligibilityResponseSerializer(serializers.Serializer):
|
|
65
|
-
"""Serializer for the complete credential eligibility response."""
|
|
66
|
-
|
|
67
|
-
context_key = serializers.CharField()
|
|
68
|
-
credentials = CredentialEligibilitySerializer(many=True)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class CredentialListResponseSerializer(serializers.Serializer):
|
|
72
|
-
"""Serializer for credential list response."""
|
|
73
|
-
|
|
74
|
-
credentials = CredentialModelSerializer(many=True)
|
learning_credentials/core_api.py
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
"""API functions for the Learning Credentials app."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
from .models import Credential, CredentialConfiguration
|
|
7
|
-
from .tasks import generate_credential_for_user_task
|
|
8
|
-
|
|
9
|
-
if TYPE_CHECKING:
|
|
10
|
-
from django.contrib.auth.models import User
|
|
11
|
-
from opaque_keys.edx.keys import CourseKey
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_eligible_users_by_credential_type(
|
|
17
|
-
course_id: 'CourseKey', user_id: int | None = None
|
|
18
|
-
) -> dict[str, list['User']]:
|
|
19
|
-
"""
|
|
20
|
-
Retrieve eligible users for each credential type in the given course.
|
|
21
|
-
|
|
22
|
-
:param course_id: The key of the course for which to check eligibility.
|
|
23
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
24
|
-
:return: A dictionary with credential type as the key and eligible users as the value.
|
|
25
|
-
"""
|
|
26
|
-
credential_configs = CredentialConfiguration.objects.filter(course_id=course_id)
|
|
27
|
-
|
|
28
|
-
if not credential_configs:
|
|
29
|
-
return {}
|
|
30
|
-
|
|
31
|
-
eligible_users_by_type = {}
|
|
32
|
-
for credential_config in credential_configs:
|
|
33
|
-
user_ids = credential_config.get_eligible_user_ids(user_id)
|
|
34
|
-
filtered_user_ids = credential_config.filter_out_user_ids_with_credentials(user_ids)
|
|
35
|
-
|
|
36
|
-
if user_id:
|
|
37
|
-
eligible_users_by_type[credential_config.credential_type.name] = list(set(filtered_user_ids) & {user_id})
|
|
38
|
-
else:
|
|
39
|
-
eligible_users_by_type[credential_config.credential_type.name] = filtered_user_ids
|
|
40
|
-
|
|
41
|
-
return eligible_users_by_type
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def get_user_credentials_by_type(course_id: 'CourseKey', user_id: int) -> dict[str, dict[str, str]]:
|
|
45
|
-
"""
|
|
46
|
-
Retrieve the available credentials for a given user in a course.
|
|
47
|
-
|
|
48
|
-
:param course_id: The course ID for which to retrieve credentials.
|
|
49
|
-
:param user_id: The ID of the user for whom credentials are being retrieved.
|
|
50
|
-
:return: A dict where keys are credential types and values are dicts with the download link and status.
|
|
51
|
-
"""
|
|
52
|
-
credentials = Credential.objects.filter(user_id=user_id, course_id=course_id)
|
|
53
|
-
|
|
54
|
-
return {cred.credential_type: {'download_url': cred.download_url, 'status': cred.status} for cred in credentials}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def generate_credential_for_user(course_id: 'CourseKey', credential_type: str, user_id: int, force: bool = False):
|
|
58
|
-
"""
|
|
59
|
-
Generate a credential for a user in a course.
|
|
60
|
-
|
|
61
|
-
:param course_id: The course ID for which to generate the credential.
|
|
62
|
-
:param credential_type: The type of credential to generate.
|
|
63
|
-
:param user_id: The ID of the user for whom the credential is being generated.
|
|
64
|
-
:param force: If True, will generate the credential even if the user is not eligible.
|
|
65
|
-
"""
|
|
66
|
-
credential_config = CredentialConfiguration.objects.get(course_id=course_id, credential_type__name=credential_type)
|
|
67
|
-
|
|
68
|
-
if not credential_config:
|
|
69
|
-
logger.error('No course configuration found for course %s', course_id)
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
if not force and not credential_config.get_eligible_user_ids(user_id):
|
|
73
|
-
logger.error('User %s is not eligible for the credential in course %s', user_id, course_id)
|
|
74
|
-
msg = 'User is not eligible for the credential.'
|
|
75
|
-
raise ValueError(msg)
|
|
76
|
-
|
|
77
|
-
generate_credential_for_user_task.delay(credential_config.id, user_id)
|