learning-credentials 0.3.0rc4__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 +1 -11
- learning_credentials/api/v1/views.py +19 -367
- 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.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/METADATA +19 -3
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/RECORD +16 -22
- {learning_credentials-0.3.0rc4.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.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/WHEEL +0 -0
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/licenses/LICENSE.txt +0 -0
- {learning_credentials-0.3.0rc4.dist-info → learning_credentials-0.3.0rc10.dist-info}/top_level.txt +0 -0
learning_credentials/models.py
CHANGED
|
@@ -7,7 +7,7 @@ import logging
|
|
|
7
7
|
import uuid
|
|
8
8
|
from importlib import import_module
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
import jsonfield
|
|
13
13
|
from django.conf import settings
|
|
@@ -165,12 +165,11 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
165
165
|
filtered_user_ids_set = set(user_ids) - set(users_ids_with_credentials)
|
|
166
166
|
return list(filtered_user_ids_set)
|
|
167
167
|
|
|
168
|
-
def
|
|
168
|
+
def get_eligible_user_ids(self) -> list[int]:
|
|
169
169
|
"""
|
|
170
|
-
|
|
170
|
+
Get the list of eligible learners for the given course.
|
|
171
171
|
|
|
172
|
-
:
|
|
173
|
-
:return: Raw result from the retrieval function - list of user IDs or user details dict.
|
|
172
|
+
:return: A list of user IDs.
|
|
174
173
|
"""
|
|
175
174
|
func_path = self.credential_type.retrieval_func
|
|
176
175
|
module_path, func_name = func_path.rsplit('.', 1)
|
|
@@ -178,40 +177,7 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
178
177
|
func = getattr(module, func_name)
|
|
179
178
|
|
|
180
179
|
custom_options = {**self.credential_type.custom_options, **self.custom_options}
|
|
181
|
-
return func(self.learning_context_key, custom_options
|
|
182
|
-
|
|
183
|
-
def get_eligible_user_ids(self, user_id: int | None = None) -> list[int]:
|
|
184
|
-
"""
|
|
185
|
-
Get the list of eligible learners for the given course.
|
|
186
|
-
|
|
187
|
-
:param user_id: Optional. If provided, will check eligibility for the specific user.
|
|
188
|
-
:return: A list of user IDs.
|
|
189
|
-
"""
|
|
190
|
-
result = self._call_retrieval_func(user_id)
|
|
191
|
-
|
|
192
|
-
if user_id is not None:
|
|
193
|
-
# Single user case: return list with user ID if eligible
|
|
194
|
-
if isinstance(result, dict) and result.get('is_eligible', False):
|
|
195
|
-
return [user_id]
|
|
196
|
-
return []
|
|
197
|
-
|
|
198
|
-
# Multiple users case: result should already be a list of user IDs
|
|
199
|
-
return result if isinstance(result, list) else []
|
|
200
|
-
|
|
201
|
-
def get_user_eligibility_details(self, user_id: int) -> dict[str, Any]:
|
|
202
|
-
"""
|
|
203
|
-
Get detailed eligibility information for a specific user.
|
|
204
|
-
|
|
205
|
-
:param user_id: The user ID to check eligibility for.
|
|
206
|
-
:return: Dictionary containing eligibility details and progress information.
|
|
207
|
-
"""
|
|
208
|
-
result = self._call_retrieval_func(user_id)
|
|
209
|
-
|
|
210
|
-
if isinstance(result, dict):
|
|
211
|
-
return result
|
|
212
|
-
|
|
213
|
-
# Fallback for processors that don't support detailed results
|
|
214
|
-
return {'is_eligible': False}
|
|
180
|
+
return func(self.learning_context_key, custom_options)
|
|
215
181
|
|
|
216
182
|
def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0):
|
|
217
183
|
"""
|
|
@@ -328,15 +294,15 @@ class Credential(TimeStampedModel):
|
|
|
328
294
|
learning_context_name = get_learning_context_name(self.learning_context_key)
|
|
329
295
|
user = get_user_model().objects.get(id=self.user_id)
|
|
330
296
|
msg = Message(
|
|
331
|
-
name="certificate_generated",
|
|
332
|
-
app_label="learning_credentials",
|
|
333
|
-
recipient=Recipient(lms_user_id=user.id, email_address=user.email),
|
|
334
|
-
language='en',
|
|
297
|
+
name="certificate_generated",
|
|
298
|
+
app_label="learning_credentials",
|
|
299
|
+
recipient=Recipient(lms_user_id=user.id, email_address=user.email),
|
|
300
|
+
language='en',
|
|
335
301
|
context={
|
|
336
302
|
'certificate_link': self.download_url,
|
|
337
303
|
'course_name': learning_context_name,
|
|
338
304
|
'platform_name': settings.PLATFORM_NAME,
|
|
339
|
-
},
|
|
305
|
+
},
|
|
340
306
|
)
|
|
341
307
|
ace.send(msg)
|
|
342
308
|
|
|
@@ -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.0rc4.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=OBORrqOVm1OhqGoqXp1yYt-6FqnXs8LdqAmDSs3g_dM,16319
|
|
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_blank=True, 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)
|