learning-credentials 0.2.3__py3-none-any.whl → 0.3.0rc1__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.
Files changed (26) hide show
  1. learning_credentials/api/__init__.py +1 -0
  2. learning_credentials/api/urls.py +12 -0
  3. learning_credentials/api/v1/__init__.py +1 -0
  4. learning_credentials/api/v1/permissions.py +60 -0
  5. learning_credentials/api/v1/serializers.py +74 -0
  6. learning_credentials/api/v1/urls.py +17 -0
  7. learning_credentials/api/v1/views.py +357 -0
  8. learning_credentials/apps.py +10 -0
  9. learning_credentials/compat.py +11 -2
  10. learning_credentials/core_api.py +77 -0
  11. learning_credentials/generators.py +3 -1
  12. learning_credentials/models.py +44 -10
  13. learning_credentials/processors.py +148 -40
  14. learning_credentials/public/css/credentials_xblock.css +7 -0
  15. learning_credentials/public/html/credentials_xblock.html +48 -0
  16. learning_credentials/public/js/credentials_xblock.js +23 -0
  17. learning_credentials/urls.py +3 -5
  18. learning_credentials/xblocks.py +85 -0
  19. {learning_credentials-0.2.3.dist-info → learning_credentials-0.3.0rc1.dist-info}/METADATA +2 -1
  20. {learning_credentials-0.2.3.dist-info → learning_credentials-0.3.0rc1.dist-info}/RECORD +24 -13
  21. learning_credentials-0.3.0rc1.dist-info/entry_points.txt +8 -0
  22. learning_credentials/views.py +0 -1
  23. learning_credentials-0.2.3.dist-info/entry_points.txt +0 -2
  24. {learning_credentials-0.2.3.dist-info → learning_credentials-0.3.0rc1.dist-info}/WHEEL +0 -0
  25. {learning_credentials-0.2.3.dist-info → learning_credentials-0.3.0rc1.dist-info}/licenses/LICENSE.txt +0 -0
  26. {learning_credentials-0.2.3.dist-info → learning_credentials-0.3.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ """Learning Credentials API package."""
@@ -0,0 +1,12 @@
1
+ """API URLs."""
2
+
3
+ from django.urls import include, path
4
+
5
+ urlpatterns = [
6
+ path(
7
+ "v1/",
8
+ include(
9
+ ("learning_credentials.api.v1.urls", "learning_credentials_api_v1"), namespace="learning_credentials_api_v1"
10
+ ),
11
+ ),
12
+ ]
@@ -0,0 +1 @@
1
+ """Learning Credentials API v1 package."""
@@ -0,0 +1,60 @@
1
+ """Django REST framework permissions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from learning_paths.models import LearningPathEnrollment
6
+ from opaque_keys import InvalidKeyError
7
+ from opaque_keys.edx.keys import LearningContextKey
8
+ from rest_framework.exceptions import NotFound, ParseError
9
+ from rest_framework.permissions import BasePermission
10
+
11
+ from learning_credentials.compat import get_course_enrollments
12
+
13
+ if TYPE_CHECKING:
14
+ from rest_framework.request import Request
15
+ from rest_framework.views import APIView
16
+
17
+
18
+ class IsAdminOrSelf(BasePermission):
19
+ """
20
+ Permission to allow only admins or the user themselves to access the API.
21
+
22
+ Non-staff users cannot pass "username" that is not their own.
23
+ """
24
+
25
+ def has_permission(self, request: "Request", view: "APIView") -> bool: # noqa: ARG002
26
+ """Check if the user is admin or accessing their own data."""
27
+ if request.user.is_staff:
28
+ return True
29
+
30
+ username = request.query_params.get("username") if request.method == "GET" else request.data.get("username")
31
+
32
+ # For learners, the username passed should match the logged in user.
33
+ if username:
34
+ return request.user.username == username
35
+ return True
36
+
37
+
38
+ class CanAccessLearningContext(BasePermission):
39
+ """Permission to allow access to learning context if the user is enrolled."""
40
+
41
+ def has_permission(self, request: "Request", view: "APIView") -> bool:
42
+ """Check if the user is enrolled in the learning context."""
43
+ try:
44
+ key = view.kwargs.get("learning_context_key") or request.query_params.get("learning_context_key")
45
+ learning_context_key = LearningContextKey.from_string(key)
46
+ except InvalidKeyError as e:
47
+ msg = "Invalid learning context key."
48
+ raise ParseError(msg) from e
49
+
50
+ if learning_context_key.is_course:
51
+ if bool(get_course_enrollments(learning_context_key, request.user.id)):
52
+ return True
53
+ msg = "Course not found or user does not have access."
54
+ raise NotFound(msg)
55
+
56
+ if LearningPathEnrollment.objects.filter(learning_path__key=learning_context_key, user=request.user).exists():
57
+ return True
58
+
59
+ msg = "Learning path not found or user does not have access."
60
+ raise NotFound(msg)
@@ -0,0 +1,74 @@
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)
@@ -0,0 +1,17 @@
1
+ """API v1 URLs."""
2
+
3
+ from django.urls import path
4
+
5
+ from .views import CredentialEligibilityView, CredentialListView
6
+
7
+ urlpatterns = [
8
+ # Credential eligibility endpoints
9
+ path('eligibility/<str:learning_context_key>/', CredentialEligibilityView.as_view(), name='credential-eligibility'),
10
+ path(
11
+ 'eligibility/<str:learning_context_key>/<int:credential_type_id>/',
12
+ CredentialEligibilityView.as_view(),
13
+ name='credential-generation',
14
+ ),
15
+ # Credential listing endpoints
16
+ path('credentials/', CredentialListView.as_view(), name='credential-list'),
17
+ ]
@@ -0,0 +1,357 @@
1
+ """API views for Learning Credentials."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ import edx_api_doc_tools as apidocs
7
+ from django.contrib.auth.models import User
8
+ from django.shortcuts import get_object_or_404
9
+ from edx_api_doc_tools import ParameterLocation
10
+ from rest_framework import status
11
+ from rest_framework.permissions import IsAuthenticated
12
+ from rest_framework.response import Response
13
+ from rest_framework.views import APIView
14
+
15
+ from learning_credentials.models import Credential, CredentialConfiguration
16
+ from learning_credentials.tasks import generate_credential_for_user_task
17
+
18
+ from .permissions import CanAccessLearningContext, IsAdminOrSelf
19
+ from .serializers import (
20
+ CredentialEligibilityResponseSerializer,
21
+ CredentialListResponseSerializer,
22
+ CredentialModelSerializer,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from rest_framework.request import Request
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class CredentialEligibilityView(APIView):
32
+ """
33
+ API view for credential eligibility and generation.
34
+
35
+ This endpoint manages credential eligibility checking and generation for users in specific learning contexts.
36
+
37
+ Supported Learning Contexts:
38
+ - Course keys: `course-v1:org+course+run`
39
+ - Learning path keys: `path-v1:org+path+run+group`
40
+ """
41
+
42
+ permission_classes = (IsAuthenticated, IsAdminOrSelf, CanAccessLearningContext)
43
+
44
+ def _get_eligibility_data(self, user: User, config: CredentialConfiguration) -> dict:
45
+ """Calculate eligibility data for a credential configuration."""
46
+ progress_data = config.get_user_eligibility_details(user_id=user.id) # ty: ignore[unresolved-attribute]
47
+
48
+ existing_credential = (
49
+ Credential.objects.filter(
50
+ user_id=user.id, # ty: ignore[unresolved-attribute]
51
+ learning_context_key=config.learning_context_key,
52
+ credential_type=config.credential_type.name,
53
+ )
54
+ .exclude(status=Credential.Status.ERROR)
55
+ .first()
56
+ )
57
+
58
+ return {
59
+ 'credential_type_id': config.credential_type.pk,
60
+ 'name': config.credential_type.name,
61
+ 'is_eligible': progress_data.get('is_eligible', False),
62
+ 'existing_credential': existing_credential.uuid if existing_credential else None,
63
+ 'existing_credential_url': existing_credential.download_url if existing_credential else None,
64
+ **progress_data,
65
+ }
66
+
67
+ @apidocs.schema(
68
+ parameters=[
69
+ apidocs.string_parameter(
70
+ "learning_context_key",
71
+ ParameterLocation.PATH,
72
+ description=(
73
+ "Learning context identifier. Can be a course key (course-v1:OpenedX+DemoX+DemoCourse) "
74
+ "or learning path key (path-v1:OpenedX+DemoX+DemoPath+Demo)"
75
+ ),
76
+ ),
77
+ ],
78
+ responses={
79
+ 200: CredentialEligibilityResponseSerializer,
80
+ 400: "Invalid context key format.",
81
+ 403: "User is not authenticated.",
82
+ 404: "Learning context not found or user does not have access.",
83
+ },
84
+ )
85
+ def get(self, request: "Request", learning_context_key: str) -> Response:
86
+ """
87
+ Get credential eligibility for a learning context.
88
+
89
+ Retrieve detailed eligibility information for all available credentials in a learning context.
90
+ This endpoint returns comprehensive progress data including:
91
+ - Current grades and requirements for grade-based credentials
92
+ - Completion percentages for completion-based credentials
93
+ - Step-by-step progress for learning paths
94
+ - Eligibility status for each credential type
95
+
96
+ **Example Request**
97
+
98
+ GET /api/learning_credentials/v1/eligibility/course-v1:OpenedX+DemoX+DemoCourse/
99
+
100
+ **Response Values**
101
+
102
+ If the request is successful, an HTTP 200 "OK" response is returned.
103
+
104
+ The response structure adapts based on credential types:
105
+ - Grade-based credentials include `current_grades` and `required_grades`
106
+ - Completion-based credentials include `current_completion` and `required_completion`
107
+ - Learning paths include detailed `steps` breakdown
108
+
109
+ **Example Response for Grade-based Credential**
110
+
111
+ ```json
112
+ {
113
+ "context_key": "course-v1:OpenedX+DemoX+DemoCourse",
114
+ "credentials": [
115
+ {
116
+ "credential_type_id": 1,
117
+ "name": "Certificate of Achievement",
118
+ "description": "",
119
+ "is_eligible": true,
120
+ "existing_credential": null,
121
+ "current_grades": {
122
+ "Final Exam": 86,
123
+ "Overall Grade": 82
124
+ },
125
+ "required_grades": {
126
+ "Final Exam": 65,
127
+ "Overall Grade": 80
128
+ }
129
+ }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ **Example Response for Completion-based Credential**
135
+
136
+ ```json
137
+ {
138
+ "context_key": "course-v1:OpenedX+DemoX+DemoCourse",
139
+ "credentials": [
140
+ {
141
+ "credential_type_id": 2,
142
+ "name": "Certificate of Completion",
143
+ "description": "",
144
+ "is_eligible": false,
145
+ "existing_credential": null,
146
+ "current_completion": 74.0,
147
+ "required_completion": 100.0
148
+ }
149
+ ]
150
+ }
151
+ ```
152
+ """
153
+ configurations = CredentialConfiguration.objects.filter(
154
+ learning_context_key=learning_context_key
155
+ ).select_related('credential_type')
156
+
157
+ eligibility_data = [self._get_eligibility_data(request.user, config) for config in configurations]
158
+
159
+ response_data = {
160
+ 'context_key': learning_context_key,
161
+ 'credentials': eligibility_data,
162
+ }
163
+
164
+ serializer = CredentialEligibilityResponseSerializer(data=response_data)
165
+ serializer.is_valid(raise_exception=True)
166
+ return Response(serializer.data)
167
+
168
+ @apidocs.schema(
169
+ parameters=[
170
+ apidocs.string_parameter(
171
+ "learning_context_key",
172
+ ParameterLocation.PATH,
173
+ description="Learning context identifier (e.g. course-v1:OpenedX+DemoX+DemoCourse)",
174
+ ),
175
+ apidocs.parameter(
176
+ "credential_type_id",
177
+ ParameterLocation.PATH,
178
+ int,
179
+ description="ID of the credential type to generate",
180
+ ),
181
+ ],
182
+ responses={
183
+ 201: "Credential generation started.",
184
+ 400: "User is not eligible for this credential or validation error.",
185
+ 403: "User is not authenticated.",
186
+ 404: "Learning context or credential type not found, or user does not have access.",
187
+ 409: "User already has a valid credential of this type.",
188
+ 500: "Internal server error during credential generation.",
189
+ },
190
+ )
191
+ def post(self, request: "Request", learning_context_key: str, credential_type_id: int) -> Response:
192
+ """
193
+ Trigger credential generation for an eligible user.
194
+
195
+ This endpoint initiates the credential generation process for a specific credential type.
196
+ The user must be eligible for the credential based on the configured requirements.
197
+
198
+ **Prerequisites:**
199
+ - User must be authenticated
200
+ - User must be enrolled in the course or have access to the learning path
201
+ - User must meet the eligibility criteria for the specific credential type
202
+ - User must not already have an existing valid credential of this type
203
+
204
+ **Process:**
205
+ 1. Validates user eligibility using the configured processor function
206
+ 2. Checks for existing credentials to prevent duplicates
207
+ 3. Initiates asynchronous credential generation
208
+ 4. Returns credential status and tracking information
209
+
210
+ **Notification:**
211
+ Users will receive an email notification when credential generation completes.
212
+
213
+ **Example Request**
214
+
215
+ POST /api/learning_credentials/v1/eligibility/course-v1:OpenedX+DemoX+DemoCourse/1/
216
+
217
+ **Response Values**
218
+
219
+ If the request is successful, an HTTP 201 "Created" response is returned.
220
+
221
+ **Example Response**
222
+
223
+ ```json
224
+ {
225
+ "status": "generating",
226
+ "credential_id": "123e4567-e89b-12d3-a456-426614174000",
227
+ "message": "Credential generation started. You will receive an email when ready."
228
+ }
229
+ ```
230
+ """
231
+ config = get_object_or_404(
232
+ CredentialConfiguration.objects.select_related('credential_type'),
233
+ learning_context_key=learning_context_key,
234
+ credential_type_id=credential_type_id,
235
+ )
236
+
237
+ existing_credential = (
238
+ Credential.objects.filter(
239
+ user_id=request.user.id,
240
+ learning_context_key=learning_context_key,
241
+ credential_type=config.credential_type.name,
242
+ )
243
+ .exclude(status=Credential.Status.ERROR)
244
+ .first()
245
+ )
246
+
247
+ if existing_credential:
248
+ return Response({"detail": "User already has a credential of this type."}, status=status.HTTP_409_CONFLICT)
249
+
250
+ if not config.get_eligible_user_ids(user_id=request.user.id):
251
+ return Response({"detail": "User is not eligible for this credential."}, status=status.HTTP_400_BAD_REQUEST)
252
+
253
+ generate_credential_for_user_task.delay(config.id, request.user.id)
254
+ return Response({"detail": "Credential generation started."}, status=status.HTTP_201_CREATED)
255
+
256
+
257
+ class CredentialListView(APIView):
258
+ """
259
+ API view to list user credentials with staff override capability.
260
+
261
+ This endpoint provides access to user credential records with optional filtering
262
+ by learning context and staff oversight capabilities.
263
+
264
+ **Authentication Required**: Yes
265
+
266
+ **Staff Features**:
267
+ - Staff users can view credentials for any user by providing `username` parameter
268
+ - Non-staff users can only view their own credentials
269
+ """
270
+
271
+ def get_permissions(self) -> list:
272
+ """Instantiate and return the list of permissions required for this view."""
273
+ permission_classes = [IsAuthenticated, IsAdminOrSelf]
274
+
275
+ if self.request.query_params.get('learning_context_key'):
276
+ permission_classes.append(CanAccessLearningContext)
277
+
278
+ return [permission() for permission in permission_classes]
279
+
280
+ @apidocs.schema(
281
+ parameters=[
282
+ apidocs.string_parameter(
283
+ "learning_context_key",
284
+ ParameterLocation.QUERY,
285
+ description="Optional learning context to filter credentials (e.g. course-v1:OpenedX+DemoX+DemoCourse)",
286
+ ),
287
+ apidocs.string_parameter(
288
+ "username",
289
+ ParameterLocation.QUERY,
290
+ description="Username to view credentials for (staff only)",
291
+ ),
292
+ ],
293
+ responses={
294
+ 200: CredentialListResponseSerializer,
295
+ 403: "User is not authenticated or lacks permission to view specified user's credentials.",
296
+ 404: "Specified user not found or learning context not found/accessible.",
297
+ },
298
+ )
299
+ def get(self, request: "Request") -> Response:
300
+ """
301
+ Retrieve a list of credentials for the authenticated user or a specified user.
302
+
303
+ This endpoint returns credential records with filtering options:
304
+ - Filter by learning context (course or learning path)
305
+ - Staff users can view credentials for any user
306
+ - Regular users can only view their own credentials
307
+
308
+ **Query Parameters:**
309
+ - `username` (staff only): View credentials for a specific user
310
+
311
+ **Path Parameters:**
312
+ - `learning_context_key` (optional): Filter credentials by learning context
313
+
314
+ **Response includes:**
315
+ - Credential ID and type
316
+ - Learning context information
317
+ - Creation date and status
318
+ - Download URL for completed credentials
319
+
320
+ **Example Request**
321
+
322
+ GET /api/learning_credentials/v1/credentials/
323
+ GET /api/learning_credentials/v1/credentials/course-v1:OpenedX+DemoX+DemoCourse/
324
+ GET /api/learning_credentials/v1/credentials/?username=student123 # staff only
325
+
326
+ **Response Values**
327
+
328
+ If the request is successful, an HTTP 200 "OK" response is returned.
329
+
330
+ **Example Response**
331
+
332
+ ```json
333
+ {
334
+ "credentials": [
335
+ {
336
+ "credential_id": "123e4567-e89b-12d3-a456-426614174000",
337
+ "credential_type": "Certificate of Achievement",
338
+ "context_key": "course-v1:OpenedX+DemoX+DemoCourse",
339
+ "status": "available",
340
+ "created_date": "2024-08-20T10:30:00Z",
341
+ "download_url": "https://example.com/credentials/123e4567.pdf"
342
+ }
343
+ ]
344
+ }
345
+ ```
346
+ """
347
+ learning_context_key = request.query_params.get('learning_context_key')
348
+ username = request.query_params.get('username')
349
+ user = get_object_or_404(User, username=username) if username else request.user
350
+
351
+ credentials_queryset = Credential.objects.filter(user_id=user.pk)
352
+
353
+ if learning_context_key:
354
+ credentials_queryset = credentials_queryset.filter(learning_context_key=learning_context_key)
355
+
356
+ credentials_data = CredentialModelSerializer(credentials_queryset, many=True).data
357
+ return Response({'credentials': credentials_data})
@@ -15,10 +15,20 @@ class LearningCredentialsConfig(AppConfig):
15
15
 
16
16
  # https://edx.readthedocs.io/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html
17
17
  plugin_app: ClassVar[dict[str, dict[str, dict]]] = {
18
+ 'url_config': {
19
+ 'lms.djangoapp': {
20
+ 'namespace': 'learning_credentials',
21
+ 'app_name': 'learning_credentials',
22
+ }
23
+ },
18
24
  'settings_config': {
19
25
  'lms.djangoapp': {
20
26
  'common': {'relative_path': 'settings.common'},
21
27
  'production': {'relative_path': 'settings.production'},
22
28
  },
29
+ 'cms.djangoapp': {
30
+ 'common': {'relative_path': 'settings.common'},
31
+ 'production': {'relative_path': 'settings.production'},
32
+ },
23
33
  },
24
34
  }
@@ -73,12 +73,21 @@ def get_learning_context_name(learning_context_key: LearningContextKey) -> str:
73
73
  return _get_learning_path_name(learning_context_key)
74
74
 
75
75
 
76
- def get_course_enrollments(course_id: CourseKey) -> list[User]:
77
- """Get the course enrollments from Open edX."""
76
+ def get_course_enrollments(course_id: CourseKey, user_id: int | None = None) -> list[User]:
77
+ """
78
+ Get the course enrollments from Open edX.
79
+
80
+ :param course_id: The course ID.
81
+ :param user_id: Optional. If provided, will filter the enrollments by user.
82
+ :return: A list of users enrolled in the course.
83
+ """
78
84
  # noinspection PyUnresolvedReferences,PyPackageRequirements
79
85
  from common.djangoapps.student.models import CourseEnrollment
80
86
 
81
87
  enrollments = CourseEnrollment.objects.filter(course_id=course_id, is_active=True).select_related('user')
88
+ if user_id:
89
+ enrollments = enrollments.filter(user__id=user_id)
90
+
82
91
  return [enrollment.user for enrollment in enrollments]
83
92
 
84
93
 
@@ -0,0 +1,77 @@
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)
@@ -89,7 +89,8 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
89
89
  pdf_canvas = canvas.Canvas(io.BytesIO(), pagesize=(template_width, template_height))
90
90
 
91
91
  # Write the learner name.
92
- pdf_canvas.setFont(font, 32)
92
+ # TODO: Add tests.
93
+ pdf_canvas.setFont(font, options.get('name_size', 32))
93
94
  name_color = options.get('name_color', '#000')
94
95
  pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color))
95
96
 
@@ -184,6 +185,7 @@ def generate_pdf_credential(
184
185
  - font: The name of the font to use.
185
186
  - name_y: The Y coordinate of the name on the credential (vertical position on the template).
186
187
  - name_color: The color of the name on the credential (hexadecimal color code).
188
+ - name_size: The font size of the name on the credential. The default value is 32.
187
189
  - context_name: Specify the custom course or Learning Path name.
188
190
  - context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
189
191
  - context_name_color: The color of the context name on the credential (hexadecimal color code).