learning-credentials 0.3.0rc4__py3-none-any.whl → 0.3.1rc1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.3.0rc4
3
+ Version: 0.3.1rc1
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,38 @@ Unreleased
175
176
 
176
177
  *
177
178
 
179
+ 0.3.1 - 2025-12-15
180
+ ******************
181
+
182
+ Added
183
+ =====
184
+
185
+ * Support for defining the course name using the ``cert_name_long`` field (in Studio's Advanced Settings).
186
+ * Support for specifying individual fonts for PDF text elements.
187
+ * Support for \n in learning context names in PDF certificates.
188
+ * Options for uppercase name and issue date in PDF certificates.
189
+ * Option for defining character spacing for issue date in PDF certificates.
190
+
191
+ Modified
192
+ ========
193
+
194
+ * Replaced ``template_two_lines`` with ``template_multiline``.
195
+
196
+ 0.3.0 - 2025-09-17
197
+ ******************
198
+
199
+ Added
200
+ =====
201
+
202
+ * REST API endpoint to check if credentials are configured for a learning context.
203
+
204
+ 0.2.4 - 2025-09-07
205
+
206
+ Added
207
+ =====
208
+
209
+ * Option to customize the learner's name size on the PDF certificate.
210
+
178
211
  0.2.3 - 2025-08-18
179
212
 
180
213
  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=trdQxe-JRhUdUaOQoQWiGL1sn6I1sfDiTvdCwy8yGuw,1037
4
+ learning_credentials/compat.py,sha256=bTAB6bTh99ZyhUqOsDtM_BuIPzFxCjySFtfvc-_fCd4,4731
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=GhKrBuPSXOkw4k1ejPTYZBJm78m-sou6ffpUNCGLm28,11492
7
+ learning_credentials/models.py,sha256=J_SCNiu42yhdi12eDMLsxNCTkJK7_vqneQjyGYG5KJ4,16048
8
+ learning_credentials/processors.py,sha256=LkdjmkLBnXc9qeMcksB1T8AQ5ZhYaECyQO__KfHB_aU,15212
10
9
  learning_credentials/tasks.py,sha256=byoFEUvN_ayVaU5K5SlEiA7vu9BRPaSSmKnB9g5toec,1927
11
- learning_credentials/urls.py,sha256=9Xc-imliMCIOWqFHfm-CSAgwm2tQGfMR18jCyBpKcho,176
12
- learning_credentials/xblocks.py,sha256=gb-bfkOlRnBzwUg7CTqBJfg-BOJ1UOtaBQRmbyGuD0w,3568
10
+ learning_credentials/urls.py,sha256=gO_c930rzMylP-riQ9SGHXH9JIMF7ajySDT2Tc-E8x4,188
13
11
  learning_credentials/api/__init__.py,sha256=q8sLFfwo5RwQu8FY6BJUL_Jrt3TUojbZK-Zlw9v08EM,40
14
- learning_credentials/api/urls.py,sha256=JfGSbzvC5d7s9dRq4C0d-AzTDuOVnen3wvFYSJQoEdQ,255
12
+ learning_credentials/api/urls.py,sha256=wW27hrrJ7D_h8PbFDbSxzeaneNla0R-56gjKy9zISG8,216
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=6YqLS4aVfA1cuLOgVe4lFUFa38wVehYKleXBF8ImMm0,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.1rc1.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
35
+ learning_credentials-0.3.1rc1.dist-info/METADATA,sha256=7QvG0e4W50YkjQ9ds4wbEVqIpAZeterqm9LAu6gZRhg,7647
36
+ learning_credentials-0.3.1rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ learning_credentials-0.3.1rc1.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
38
+ learning_credentials-0.3.1rc1.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
39
+ learning_credentials-0.3.1rc1.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)
@@ -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)
@@ -1,7 +0,0 @@
1
- .credentials-block .credentials-list .credential {
2
- padding-bottom: 20px;
3
-
4
- .credential-status {
5
- margin-bottom: 10px;
6
- }
7
- }
@@ -1,48 +0,0 @@
1
- <div class="credentials-block">
2
- {% if is_author_mode %}
3
- <p>The Studio view of this XBlock is not supported yet. Please preview the XBlock in the LMS.</p>
4
- {% else %}
5
- <h3>Check Your Certificate Eligibility Status</h3>
6
- <ul class="credentials-list">
7
- {% if credentials %}
8
- {% for credential_type, credential in credentials.items %}
9
- <li class="credential">
10
- <strong>Type:</strong> {{ credential_type }}
11
- {% if credential.download_url %}
12
- <p class="credential-status">Congratulations on finishing strong!</p>
13
- <strong>Download Link:</strong> <a href="{{ credential.download_url }}">Download Certificate</a>
14
- {% elif credential.status == credential.Status.ERROR %}
15
- <p class="credential-status">Something went wrong. Please contact us via the Help page for assistance.</p>
16
- {% endif %}
17
- <button class="btn-brand generate-credential" data-credential-type="{{ credential_type }}" disabled>
18
- Certificate Claimed
19
- </button>
20
- <div id="message-area-{{ credential_type }}"></div>
21
- </li>
22
- {% endfor %}
23
- {% endif %}
24
-
25
- {% if eligible_types %}
26
- {% for credential_type, is_eligible in eligible_types.items %}
27
- {% if not credentials or credential_type not in credentials %}
28
- <li class="credential">
29
- <strong>Type:</strong> {{ credential_type }}
30
- {% if is_eligible %}
31
- <p class="credential-status">Congratulations! You have earned this certificate. Please claim it below.</p>
32
- <button class="btn-brand generate-credential" data-credential-type="{{ credential_type }}">
33
- Claim Certificate
34
- </button>
35
- {% else %}
36
- <p class="certificate-status">You are not yet eligible for this certificate.</p>
37
- <button class="btn-brand generate-certificate" data-certificate-type="{{ credential_type }}" disabled>
38
- Claim Certificate
39
- </button>
40
- {% endif %}
41
- <div id="message-area-{{ credential_type }}"></div>
42
- </li>
43
- {% endif %}
44
- {% endfor %}
45
- {% endif %}
46
- </ul>
47
- {% endif %}
48
- </div>
@@ -1,23 +0,0 @@
1
- function CredentialsXBlock(runtime, element) {
2
- function generateCredential(event) {
3
- const button = event.target;
4
- const credentialType = $(button).data('credential-type');
5
- const handlerUrl = runtime.handlerUrl(element, 'generate_credential');
6
-
7
- $.post(handlerUrl, JSON.stringify({ credential_type: credentialType }))
8
- .done(function(data) {
9
- const messageArea = $(element).find('#message-area-' + credentialType);
10
- if (data.status === 'success') {
11
- messageArea.html('<p style="color:green;">Certificate generation initiated successfully.</p>');
12
- } else {
13
- messageArea.html('<p style="color:red;">' + data.message + '</p>');
14
- }
15
- })
16
- .fail(function() {
17
- const messageArea = $(element).find('#message-area-' + credentialType);
18
- messageArea.html('<p style="color:red;">An error occurred while processing your request.</p>');
19
- });
20
- }
21
-
22
- $(element).find('.generate-credential').on('click', generateCredential);
23
- }
@@ -1,85 +0,0 @@
1
- """XBlocks for Learning Credentials."""
2
-
3
- import logging
4
-
5
- from xblock.core import XBlock
6
- from xblock.fields import Scope, String
7
- from xblock.fragment import Fragment
8
- from xblock.utils.resources import ResourceLoader
9
- from xblock.utils.studio_editable import StudioEditableXBlockMixin
10
-
11
- from .core_api import generate_credential_for_user, get_eligible_users_by_credential_type, get_user_credentials_by_type
12
-
13
- loader = ResourceLoader(__name__)
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class CredentialsXBlock(StudioEditableXBlockMixin, XBlock):
18
- """XBlock that displays the credential eligibility status and allows eligible users to generate credentials."""
19
-
20
- display_name = String(
21
- help='The display name for this component.',
22
- scope=Scope.content,
23
- display_name="Display name",
24
- default='Credentials',
25
- )
26
-
27
- def student_view(self, context) -> Fragment: # noqa: ANN001, ARG002
28
- """Main view for the student. Displays the credential eligibility or ineligibility status."""
29
- fragment = Fragment()
30
- eligible_types = False
31
- credentials = []
32
-
33
- if not (is_author_mode := getattr(self.runtime, 'is_author_mode', False)):
34
- credentials = self.get_credentials()
35
- eligible_types = self.get_eligible_credential_types()
36
-
37
- # Filter out the eligible types that already have a credential generated
38
- for cred_type in credentials:
39
- if cred_type in eligible_types:
40
- del eligible_types[cred_type]
41
-
42
- fragment.add_content(
43
- loader.render_django_template(
44
- 'public/html/credentials_xblock.html',
45
- {
46
- 'credentials': credentials,
47
- 'eligible_types': eligible_types,
48
- 'is_author_mode': is_author_mode,
49
- },
50
- )
51
- )
52
-
53
- fragment.add_css_url(self.runtime.local_resource_url(self, "public/css/credentials_xblock.css"))
54
- fragment.add_javascript_url(self.runtime.local_resource_url(self, "public/js/credentials_xblock.js"))
55
- fragment.initialize_js('CredentialsXBlock')
56
- return fragment
57
-
58
- def get_eligible_credential_types(self) -> dict[str, bool]:
59
- """Retrieve the eligibility status for each credential type."""
60
- eligible_users = get_eligible_users_by_credential_type(self.runtime.course_id, user_id=self.scope_ids.user_id)
61
-
62
- return {credential_type: bool(users) for credential_type, users in eligible_users.items()}
63
-
64
- def get_credentials(self) -> dict[str, dict[str, str]]:
65
- """Retrieve the credentials for the current user in the current course."""
66
- return get_user_credentials_by_type(self.runtime.course_id, self.scope_ids.user_id)
67
-
68
- @XBlock.json_handler
69
- def generate_credential(self, data: dict, suffix: str = '') -> dict[str, str]: # noqa: ARG002
70
- """Handler for generating a credential for a specific type."""
71
- credential_type = data.get('credential_type')
72
- if not credential_type:
73
- return {'status': 'error', 'message': 'No credential type specified.'}
74
-
75
- course_id = self.runtime.course_id
76
- user_id = self.scope_ids.user_id
77
- logger.info(
78
- 'Generating a credential for user %s in course %s with type %s.', user_id, course_id, credential_type
79
- )
80
-
81
- try:
82
- generate_credential_for_user(course_id, credential_type, user_id)
83
- except ValueError as e:
84
- return {'status': 'error', 'message': str(e)}
85
- return {'status': 'success'}