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.
@@ -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, Any
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 _call_retrieval_func(self, user_id: int | None = None) -> list[int] | dict[str, Any]:
168
+ def get_eligible_user_ids(self) -> list[int]:
169
169
  """
170
- Call the retrieval function with the given parameters.
170
+ Get the list of eligible learners for the given course.
171
171
 
172
- :param user_id: Optional. If provided, will check eligibility for the specific user.
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, user_id=user_id)
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", # type: ignore[unknown-argument]
332
- app_label="learning_credentials", # type: ignore[unknown-argument]
333
- recipient=Recipient(lms_user_id=user.id, email_address=user.email), # type: ignore[unknown-argument]
334
- language='en', # type: ignore[unknown-argument]
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
- }, # type: ignore[unknown-argument]
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], int | None], dict[int, dict[str, Any]]],
41
+ course_processor: Callable[[CourseKey, dict[str, Any]], list[int]],
42
42
  options: dict[str, Any],
43
- user_id: int | None = None,
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 detailed results
50
- with step breakdown.
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 detailed progress for all users
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 dict mapping user_id to detailed progress, with step breakdown for learning paths
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, user_id) # type: ignore[invalid-argument-type]
61
+ return course_processor(learning_context_key, options)
64
62
 
65
63
  learning_path = LearningPath.objects.get(key=learning_context_key)
66
64
 
67
- step_results_by_course = {}
68
- all_user_ids = set()
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
- # TODO: Use a single Completion Aggregator request when retrieving step results for a single user.
71
- for step in learning_path.steps.all():
72
- course_options = options.get("steps", {}).get(str(step.course_key), options)
73
- step_results = course_processor(step.course_key, course_options, user_id)
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
- all_user_ids &= set(
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
- final_results = {}
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, float]]:
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 _calculate_grades_progress(
183
- user_grades: dict[str, float],
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, user_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
- results = {}
226
- for uid, user_grades in grades.items():
227
- results[uid] = _calculate_grades_progress(user_grades, required_grades, weights)
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 results
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
- detailed_results = _process_learning_context(
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 # type: ignore[invalid-assignment]
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
- course_id: CourseKey, options: dict[str, Any], user_id: int | None = None
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
- for res in response.data['results']:
349
- completions[res['username']] = res['completion']['percent']
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
- # Get all enrolled users and map usernames to user IDs
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
- detailed_results = _process_learning_context(learning_context_key, _retrieve_course_completions, options, user_id)
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 django.conf import Settings
3
+ from typing import TYPE_CHECKING
4
4
 
5
+ if TYPE_CHECKING:
6
+ from django.conf import Settings
5
7
 
6
- def plugin_settings(settings: Settings):
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 django.conf import Settings
3
+ from typing import TYPE_CHECKING
4
4
 
5
+ if TYPE_CHECKING:
6
+ from django.conf import Settings
5
7
 
6
- def plugin_settings(settings: Settings):
8
+
9
+ def plugin_settings(settings: 'Settings'):
7
10
  """
8
11
  Use the database scheduler for Celery Beat.
9
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.3.0rc4
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 security@openedx.org.
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=COijTeVYYFZgQP_-gHJNy2ITqhFT0dfimC0PtYwnynM,1136
4
- learning_credentials/compat.py,sha256=g32MLgTnSZLGa6H3Qsq1WIPJXWPlFaNVuyZfbZXsKR8,4832
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=aDo1_r9IE1ad31oHmuIpU0MxqFV8K8ymMVVGfNdeXlg,9098
8
- learning_credentials/models.py,sha256=GurLaQ2-hY7bflOYKV5vwqTnNwM6puE0tOg4KZ8-bqQ,17694
9
- learning_credentials/processors.py,sha256=CfHuotWjCsg_Yx-uyZNTKzSREJj3xXD_LnTWUw3g2ms,19806
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=DZMa2A6Q1ltPdWCksr7I8Ltv_qV46B9yvMM0UXXWo8E,2379
17
- learning_credentials/api/v1/serializers.py,sha256=etBfIlTnb3vC8GcwTOQUUVRnTP3d_f_7Ca-azCm2O_0,2703
18
- learning_credentials/api/v1/urls.py,sha256=sS7KK-GUgKsGYmhbCqx86O3SWofNvg1KnxeBAJpzwtk,831
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=4n9AeQD-GB2MYFVrwXWEsTSrKC9btn8bgyr9OQuXNsY,302
33
- learning_credentials/settings/production.py,sha256=yEvsCldHOdsIswW7TPLW__b9YNEK-Qy05rX5WSAcEeo,484
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.0rc4.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
41
- learning_credentials-0.3.0rc4.dist-info/METADATA,sha256=Twx2Sgek5BKOv8UZYWsIQd0Z5Ph-1sONWyjIfY61_2I,6864
42
- learning_credentials-0.3.0rc4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
43
- learning_credentials-0.3.0rc4.dist-info/entry_points.txt,sha256=6aT7brHXkmLp829ZgV8t9R__wXLsLOsYKmZWOvPlGyc,166
44
- learning_credentials-0.3.0rc4.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
45
- learning_credentials-0.3.0rc4.dist-info/RECORD,,
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,5 +1,2 @@
1
1
  [lms.djangoapp]
2
2
  learning_credentials = learning_credentials.apps:LearningCredentialsConfig
3
-
4
- [xblock.v1]
5
- certificates = learning_credentials.xblocks:CredentialsXBlock
@@ -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)