learning-credentials 0.3.0rc4__tar.gz → 0.3.1rc1__tar.gz
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-0.3.0rc4 → learning_credentials-0.3.1rc1}/CHANGELOG.rst +32 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/MANIFEST.in +0 -2
- {learning_credentials-0.3.0rc4/learning_credentials.egg-info → learning_credentials-0.3.1rc1}/PKG-INFO +36 -3
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/README.rst +1 -1
- learning_credentials-0.3.1rc1/learning_credentials/api/urls.py +9 -0
- learning_credentials-0.3.1rc1/learning_credentials/api/v1/permissions.py +81 -0
- learning_credentials-0.3.1rc1/learning_credentials/api/v1/urls.py +13 -0
- learning_credentials-0.3.1rc1/learning_credentials/api/v1/views.py +83 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/apps.py +7 -10
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/compat.py +7 -10
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/generators.py +66 -31
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/models.py +11 -45
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/processors.py +40 -148
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/settings/common.py +5 -2
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/settings/production.py +5 -2
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/urls.py +3 -1
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1/learning_credentials.egg-info}/PKG-INFO +36 -3
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials.egg-info/SOURCES.txt +0 -7
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials.egg-info/entry_points.txt +0 -3
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials.egg-info/requires.txt +2 -1
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/pyproject.toml +31 -15
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/tests/test_generators.py +138 -24
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/tests/test_models.py +1 -28
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/tests/test_processors.py +33 -282
- learning_credentials-0.3.1rc1/tests/test_views.py +304 -0
- learning_credentials-0.3.0rc4/learning_credentials/api/urls.py +0 -12
- learning_credentials-0.3.0rc4/learning_credentials/api/v1/permissions.py +0 -63
- learning_credentials-0.3.0rc4/learning_credentials/api/v1/serializers.py +0 -74
- learning_credentials-0.3.0rc4/learning_credentials/api/v1/urls.py +0 -23
- learning_credentials-0.3.0rc4/learning_credentials/api/v1/views.py +0 -431
- learning_credentials-0.3.0rc4/learning_credentials/core_api.py +0 -77
- learning_credentials-0.3.0rc4/learning_credentials/public/css/credentials_xblock.css +0 -7
- learning_credentials-0.3.0rc4/learning_credentials/public/html/credentials_xblock.html +0 -48
- learning_credentials-0.3.0rc4/learning_credentials/public/js/credentials_xblock.js +0 -23
- learning_credentials-0.3.0rc4/learning_credentials/xblocks.py +0 -85
- learning_credentials-0.3.0rc4/tests/test_serializers.py +0 -205
- learning_credentials-0.3.0rc4/tests/test_views.py +0 -934
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/LICENSE.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/__init__.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/admin.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/api/__init__.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/api/v1/__init__.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/conf/locale/config.yaml +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/exceptions.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0001_initial.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/migrations/__init__.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/settings/__init__.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/tasks.py +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/base.html +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials.egg-info/dependency_links.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials.egg-info/top_level.txt +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/setup.cfg +0 -0
- {learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/tests/test_tasks.py +0 -0
|
@@ -16,6 +16,38 @@ Unreleased
|
|
|
16
16
|
|
|
17
17
|
*
|
|
18
18
|
|
|
19
|
+
0.3.1 - 2025-12-15
|
|
20
|
+
******************
|
|
21
|
+
|
|
22
|
+
Added
|
|
23
|
+
=====
|
|
24
|
+
|
|
25
|
+
* Support for defining the course name using the ``cert_name_long`` field (in Studio's Advanced Settings).
|
|
26
|
+
* Support for specifying individual fonts for PDF text elements.
|
|
27
|
+
* Support for \n in learning context names in PDF certificates.
|
|
28
|
+
* Options for uppercase name and issue date in PDF certificates.
|
|
29
|
+
* Option for defining character spacing for issue date in PDF certificates.
|
|
30
|
+
|
|
31
|
+
Modified
|
|
32
|
+
========
|
|
33
|
+
|
|
34
|
+
* Replaced ``template_two_lines`` with ``template_multiline``.
|
|
35
|
+
|
|
36
|
+
0.3.0 - 2025-09-17
|
|
37
|
+
******************
|
|
38
|
+
|
|
39
|
+
Added
|
|
40
|
+
=====
|
|
41
|
+
|
|
42
|
+
* REST API endpoint to check if credentials are configured for a learning context.
|
|
43
|
+
|
|
44
|
+
0.2.4 - 2025-09-07
|
|
45
|
+
|
|
46
|
+
Added
|
|
47
|
+
=====
|
|
48
|
+
|
|
49
|
+
* Option to customize the learner's name size on the PDF certificate.
|
|
50
|
+
|
|
19
51
|
0.2.3 - 2025-08-18
|
|
20
52
|
|
|
21
53
|
Modified
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
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
|
|
@@ -86,7 +86,7 @@ file in this repo.
|
|
|
86
86
|
Reporting Security Issues
|
|
87
87
|
*************************
|
|
88
88
|
|
|
89
|
-
Please do not report security issues in public. Please email
|
|
89
|
+
Please do not report security issues in public. Please email help@opencraft.com.
|
|
90
90
|
|
|
91
91
|
.. |pypi-badge| image:: https://img.shields.io/pypi/v/learning-credentials.svg
|
|
92
92
|
:target: https://pypi.python.org/pypi/learning-credentials/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Django REST framework permissions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.db.models import Q
|
|
6
|
+
from learning_paths.models import LearningPath
|
|
7
|
+
from opaque_keys import InvalidKeyError
|
|
8
|
+
from opaque_keys.edx.keys import LearningContextKey
|
|
9
|
+
from rest_framework.exceptions import NotFound, ParseError
|
|
10
|
+
from rest_framework.permissions import BasePermission
|
|
11
|
+
|
|
12
|
+
from learning_credentials.compat import get_course_enrollments
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from django.contrib.auth.models import User
|
|
16
|
+
from learning_paths.keys import LearningPathKey
|
|
17
|
+
from opaque_keys.edx.keys import CourseKey
|
|
18
|
+
from rest_framework.request import Request
|
|
19
|
+
from rest_framework.views import APIView
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CanAccessLearningContext(BasePermission):
|
|
23
|
+
"""Permission to allow access to learning context if the user is enrolled."""
|
|
24
|
+
|
|
25
|
+
def has_permission(self, request: "Request", view: "APIView") -> bool:
|
|
26
|
+
"""Check if the user can access the learning context."""
|
|
27
|
+
try:
|
|
28
|
+
key = view.kwargs.get("learning_context_key") or request.query_params.get("learning_context_key")
|
|
29
|
+
learning_context_key = LearningContextKey.from_string(key)
|
|
30
|
+
except InvalidKeyError as e:
|
|
31
|
+
msg = "Invalid learning context key."
|
|
32
|
+
raise ParseError(msg) from e
|
|
33
|
+
|
|
34
|
+
if request.user.is_staff:
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
if learning_context_key.is_course:
|
|
38
|
+
if self._can_access_course(learning_context_key, request.user):
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
msg = "Course not found or user does not have access."
|
|
42
|
+
raise NotFound(msg)
|
|
43
|
+
|
|
44
|
+
# For learning paths, check enrollment or if it's not invite-only.
|
|
45
|
+
if self._can_access_learning_path(learning_context_key, request.user):
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
msg = "Learning path not found or user does not have access."
|
|
49
|
+
raise NotFound(msg)
|
|
50
|
+
|
|
51
|
+
def _can_access_course(self, course_key: "CourseKey", user: "User") -> bool:
|
|
52
|
+
"""Check if user can access a course."""
|
|
53
|
+
# Check if user is enrolled in the course.
|
|
54
|
+
if get_course_enrollments(course_key, user.id): # ty: ignore[unresolved-attribute]
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
# Check if the course is a part of a learning path the user can access.
|
|
58
|
+
return self._can_access_course_via_learning_path(course_key, user)
|
|
59
|
+
|
|
60
|
+
def _get_accessible_learning_paths_filter(self, user: "User") -> Q:
|
|
61
|
+
"""Get Q filter for learning paths that the user can access."""
|
|
62
|
+
return Q(invite_only=False) | Q(learningpathenrollment__user=user, learningpathenrollment__is_active=True)
|
|
63
|
+
|
|
64
|
+
def _can_access_course_via_learning_path(self, course_key: "CourseKey", user: "User") -> bool:
|
|
65
|
+
"""Check if user can access a course through learning path membership."""
|
|
66
|
+
accessible_paths = (
|
|
67
|
+
LearningPath.objects.filter(steps__course_key=course_key)
|
|
68
|
+
.filter(self._get_accessible_learning_paths_filter(user))
|
|
69
|
+
.distinct()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return accessible_paths.exists()
|
|
73
|
+
|
|
74
|
+
def _can_access_learning_path(self, learning_path_key: "LearningPathKey", user: "User") -> bool:
|
|
75
|
+
"""Check if user can access a learning path."""
|
|
76
|
+
# Single query to check if learning path exists and user can access it
|
|
77
|
+
accessible_path = LearningPath.objects.filter(key=learning_path_key).filter(
|
|
78
|
+
self._get_accessible_learning_paths_filter(user)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return accessible_path.exists()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""API v1 URLs."""
|
|
2
|
+
|
|
3
|
+
from django.urls import path
|
|
4
|
+
|
|
5
|
+
from .views import CredentialConfigurationCheckView
|
|
6
|
+
|
|
7
|
+
urlpatterns = [
|
|
8
|
+
path(
|
|
9
|
+
'configured/<str:learning_context_key>/',
|
|
10
|
+
CredentialConfigurationCheckView.as_view(),
|
|
11
|
+
name='credential_configuration_check',
|
|
12
|
+
),
|
|
13
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""API views for Learning Credentials."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import edx_api_doc_tools as apidocs
|
|
6
|
+
from edx_api_doc_tools import ParameterLocation
|
|
7
|
+
from rest_framework import status
|
|
8
|
+
from rest_framework.permissions import IsAuthenticated
|
|
9
|
+
from rest_framework.response import Response
|
|
10
|
+
from rest_framework.views import APIView
|
|
11
|
+
|
|
12
|
+
from learning_credentials.models import CredentialConfiguration
|
|
13
|
+
|
|
14
|
+
from .permissions import CanAccessLearningContext
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from rest_framework.request import Request
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CredentialConfigurationCheckView(APIView):
|
|
21
|
+
"""API view to check if any credentials are configured for a specific learning context."""
|
|
22
|
+
|
|
23
|
+
permission_classes = (IsAuthenticated, CanAccessLearningContext)
|
|
24
|
+
|
|
25
|
+
@apidocs.schema(
|
|
26
|
+
parameters=[
|
|
27
|
+
apidocs.string_parameter(
|
|
28
|
+
"learning_context_key",
|
|
29
|
+
ParameterLocation.PATH,
|
|
30
|
+
description=(
|
|
31
|
+
"Learning context identifier. Can be a course key (course-v1:OpenedX+DemoX+DemoCourse) "
|
|
32
|
+
"or learning path key (path-v1:OpenedX+DemoX+DemoPath+Demo)"
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
responses={
|
|
37
|
+
200: "Boolean indicating if credentials are configured.",
|
|
38
|
+
400: "Invalid context key format.",
|
|
39
|
+
403: "User is not authenticated or does not have permission to access the learning context.",
|
|
40
|
+
404: "Learning context not found or user does not have access.",
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
def get(self, _request: "Request", learning_context_key: str) -> Response:
|
|
44
|
+
"""
|
|
45
|
+
Check if any credentials are configured for the given learning context.
|
|
46
|
+
|
|
47
|
+
**Example Request**
|
|
48
|
+
|
|
49
|
+
``GET /api/learning_credentials/v1/configured/course-v1:OpenedX+DemoX+DemoCourse/``
|
|
50
|
+
|
|
51
|
+
**Response Values**
|
|
52
|
+
|
|
53
|
+
- **200 OK**: Request successful, returns credential configuration status.
|
|
54
|
+
- **400 Bad Request**: Invalid learning context key format.
|
|
55
|
+
- **403 Forbidden**: User is not authenticated or does not have permission to access the learning context.
|
|
56
|
+
- **404 Not Found**: Learning context not found or user does not have access.
|
|
57
|
+
|
|
58
|
+
**Example Response**
|
|
59
|
+
|
|
60
|
+
.. code-block:: json
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
"has_credentials": true,
|
|
64
|
+
"credential_count": 2
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
**Response Fields**
|
|
68
|
+
|
|
69
|
+
- ``has_credentials``: Boolean indicating if any credentials are configured
|
|
70
|
+
- ``credential_count``: Number of credential configurations available
|
|
71
|
+
|
|
72
|
+
**Note**
|
|
73
|
+
|
|
74
|
+
This endpoint does not perform learning context existence validation, so it will not return 404 for staff users.
|
|
75
|
+
"""
|
|
76
|
+
credential_count = CredentialConfiguration.objects.filter(learning_context_key=learning_context_key).count()
|
|
77
|
+
|
|
78
|
+
response_data = {
|
|
79
|
+
'has_credentials': credential_count > 0,
|
|
80
|
+
'credential_count': credential_count,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Response(response_data, status=status.HTTP_200_OK)
|
{learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/apps.py
RENAMED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import ClassVar
|
|
6
6
|
|
|
7
7
|
from django.apps import AppConfig
|
|
8
|
+
from edx_django_utils.plugins.constants import PluginSettings, PluginURLs
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class LearningCredentialsConfig(AppConfig):
|
|
@@ -15,20 +16,16 @@ class LearningCredentialsConfig(AppConfig):
|
|
|
15
16
|
|
|
16
17
|
# https://edx.readthedocs.io/projects/edx-django-utils/en/latest/plugins/how_tos/how_to_create_a_plugin_app.html
|
|
17
18
|
plugin_app: ClassVar[dict[str, dict[str, dict]]] = {
|
|
18
|
-
|
|
19
|
+
PluginURLs.CONFIG: {
|
|
19
20
|
'lms.djangoapp': {
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
PluginURLs.NAMESPACE: name,
|
|
22
|
+
PluginURLs.APP_NAME: name,
|
|
22
23
|
}
|
|
23
24
|
},
|
|
24
|
-
|
|
25
|
+
PluginSettings.CONFIG: {
|
|
25
26
|
'lms.djangoapp': {
|
|
26
|
-
'common': {
|
|
27
|
-
'production': {
|
|
27
|
+
'common': {PluginSettings.RELATIVE_PATH: 'settings.common'},
|
|
28
|
+
'production': {PluginSettings.RELATIVE_PATH: 'settings.production'},
|
|
28
29
|
},
|
|
29
|
-
# 'cms.djangoapp': {
|
|
30
|
-
# 'common': {'relative_path': 'settings.common'},
|
|
31
|
-
# 'production': {'relative_path': 'settings.production'},
|
|
32
|
-
# },
|
|
33
30
|
},
|
|
34
31
|
}
|
{learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/compat.py
RENAMED
|
@@ -52,10 +52,13 @@ def get_course_grading_policy(course_id: CourseKey) -> dict:
|
|
|
52
52
|
def _get_course_name(course_id: CourseKey) -> str:
|
|
53
53
|
"""Get the course name from Open edX."""
|
|
54
54
|
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
|
55
|
-
from openedx.core.djangoapps.content.
|
|
55
|
+
from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
name = str(course_id)
|
|
58
|
+
if course_overview := get_course_overview_or_none(course_id):
|
|
59
|
+
name = course_overview.cert_name_long or course_overview.display_name or name
|
|
60
|
+
|
|
61
|
+
return name
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
def _get_learning_path_name(learning_path_key: LearningPathKey) -> str:
|
|
@@ -74,13 +77,7 @@ def get_learning_context_name(learning_context_key: LearningContextKey) -> str:
|
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
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
|
-
"""
|
|
80
|
+
"""Get the course enrollments from Open edX."""
|
|
84
81
|
# noinspection PyUnresolvedReferences,PyPackageRequirements
|
|
85
82
|
from common.djangoapps.student.models import CourseEnrollment
|
|
86
83
|
|
{learning_credentials-0.3.0rc4 → learning_credentials-0.3.1rc1}/learning_credentials/generators.py
RENAMED
|
@@ -19,9 +19,9 @@ from django.core.files.base import ContentFile
|
|
|
19
19
|
from django.core.files.storage import FileSystemStorage, default_storage
|
|
20
20
|
from pypdf import PdfReader, PdfWriter
|
|
21
21
|
from pypdf.constants import UserAccessPermissions
|
|
22
|
-
from reportlab.pdfbase import
|
|
23
|
-
from reportlab.pdfbase.ttfonts import TTFont
|
|
24
|
-
from reportlab.pdfgen import
|
|
22
|
+
from reportlab.pdfbase.pdfmetrics import FontError, FontNotFoundError, registerFont
|
|
23
|
+
from reportlab.pdfbase.ttfonts import TTFError, TTFont
|
|
24
|
+
from reportlab.pdfgen.canvas import Canvas
|
|
25
25
|
|
|
26
26
|
from .compat import get_default_storage_url, get_learning_context_name, get_localized_credential_date
|
|
27
27
|
from .models import CredentialAsset
|
|
@@ -33,6 +33,7 @@ if TYPE_CHECKING: # pragma: no cover
|
|
|
33
33
|
|
|
34
34
|
from django.contrib.auth.models import User
|
|
35
35
|
from opaque_keys.edx.keys import CourseKey
|
|
36
|
+
from pypdf import PageObject
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def _get_user_name(user: User) -> str:
|
|
@@ -45,25 +46,29 @@ def _get_user_name(user: User) -> str:
|
|
|
45
46
|
return user.profile.name or f"{user.first_name} {user.last_name}"
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def _register_font(
|
|
49
|
+
def _register_font(font_name: Any) -> str | None: # noqa: ANN401
|
|
49
50
|
"""
|
|
50
51
|
Register a custom font if specified in options. If not specified, use the default font (Helvetica).
|
|
51
52
|
|
|
52
|
-
:param
|
|
53
|
-
:returns: The font name.
|
|
53
|
+
:param font_name: The name of the font to register.
|
|
54
|
+
:returns: The font name if registered successfully, otherwise None.
|
|
54
55
|
"""
|
|
55
|
-
if
|
|
56
|
-
|
|
56
|
+
if not font_name:
|
|
57
|
+
return None
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
try:
|
|
60
|
+
registerFont(TTFont(font_name, CredentialAsset.get_asset_by_slug(font_name)))
|
|
61
|
+
except (FontError, FontNotFoundError, TTFError):
|
|
62
|
+
log.exception("Error registering font %s", font_name)
|
|
63
|
+
else:
|
|
64
|
+
return font_name
|
|
59
65
|
|
|
60
66
|
|
|
61
|
-
def _write_text_on_template(template:
|
|
67
|
+
def _write_text_on_template(template: PageObject, username: str, context_name: str, options: dict[str, Any]) -> Canvas:
|
|
62
68
|
"""
|
|
63
69
|
Prepare a new canvas and write the user and course name onto it.
|
|
64
70
|
|
|
65
71
|
:param template: Pdf template.
|
|
66
|
-
:param font: Font name.
|
|
67
72
|
:param username: The name of the user to generate the credential for.
|
|
68
73
|
:param context_name: The name of the learning context.
|
|
69
74
|
:param options: A dictionary documented in the `generate_pdf_credential` function.
|
|
@@ -86,20 +91,26 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
|
|
|
86
91
|
return tuple(int(hex_color[i : i + 2], 16) / 255 for i in range(0, 6, 2))
|
|
87
92
|
|
|
88
93
|
template_width, template_height = template.mediabox[2:]
|
|
89
|
-
pdf_canvas =
|
|
94
|
+
pdf_canvas = Canvas(io.BytesIO(), pagesize=(template_width, template_height))
|
|
95
|
+
font = _register_font(options.get('font')) or 'Helvetica'
|
|
90
96
|
|
|
91
97
|
# Write the learner name.
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
if options.get('name_uppercase', getattr(settings, 'LEARNING_CREDENTIALS_NAME_UPPERCASE', False)):
|
|
99
|
+
username = username.upper()
|
|
100
|
+
|
|
101
|
+
name_font = _register_font(options.get('name_font')) or font
|
|
102
|
+
pdf_canvas.setFont(name_font, options.get('name_size', 32))
|
|
94
103
|
name_color = options.get('name_color', '#000')
|
|
95
104
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color))
|
|
96
105
|
|
|
97
106
|
name_x = (template_width - pdf_canvas.stringWidth(username)) / 2
|
|
98
107
|
name_y = options.get('name_y', 290)
|
|
108
|
+
|
|
99
109
|
pdf_canvas.drawString(name_x, name_y, username)
|
|
100
110
|
|
|
101
111
|
# Write the learning context name.
|
|
102
|
-
|
|
112
|
+
context_name_font = _register_font(options.get('context_name_font')) or font
|
|
113
|
+
pdf_canvas.setFont(context_name_font, options.get('context_name_size', 28))
|
|
103
114
|
context_name_color = options.get('context_name_color', '#000')
|
|
104
115
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(context_name_color))
|
|
105
116
|
|
|
@@ -114,13 +125,22 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
|
|
|
114
125
|
|
|
115
126
|
# Write the issue date.
|
|
116
127
|
issue_date = get_localized_credential_date()
|
|
117
|
-
|
|
128
|
+
if options.get('issue_date_uppercase', getattr(settings, 'LEARNING_CREDENTIALS_ISSUE_DATE_UPPERCASE', False)):
|
|
129
|
+
issue_date = issue_date.upper()
|
|
130
|
+
|
|
131
|
+
issue_date_font = _register_font(options.get('issue_date_font')) or font
|
|
132
|
+
pdf_canvas.setFont(issue_date_font, options.get('issue_date_size', 12))
|
|
118
133
|
issue_date_color = options.get('issue_date_color', '#000')
|
|
119
134
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color))
|
|
120
135
|
|
|
121
136
|
issue_date_x = (template_width - pdf_canvas.stringWidth(issue_date)) / 2
|
|
122
137
|
issue_date_y = options.get('issue_date_y', 120)
|
|
123
|
-
|
|
138
|
+
|
|
139
|
+
issue_date_char_space = options.get(
|
|
140
|
+
'issue_date_char_space', getattr(settings, 'LEARNING_CREDENTIALS_ISSUE_DATE_CHAR_SPACE', 0)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
pdf_canvas.drawString(issue_date_x, issue_date_y, issue_date, charSpace=issue_date_char_space)
|
|
124
144
|
|
|
125
145
|
return pdf_canvas
|
|
126
146
|
|
|
@@ -169,7 +189,7 @@ def generate_pdf_credential(
|
|
|
169
189
|
credential_uuid: UUID,
|
|
170
190
|
options: dict[str, Any],
|
|
171
191
|
) -> str:
|
|
172
|
-
"""
|
|
192
|
+
r"""
|
|
173
193
|
Generate a PDF credential.
|
|
174
194
|
|
|
175
195
|
:param learning_context_key: The ID of the course or learning path the credential is for.
|
|
@@ -180,33 +200,48 @@ def generate_pdf_credential(
|
|
|
180
200
|
|
|
181
201
|
Options:
|
|
182
202
|
- template: The path to the PDF template file.
|
|
183
|
-
-
|
|
184
|
-
A
|
|
185
|
-
- font: The name of the font to use.
|
|
203
|
+
- template_multiline: The path to the PDF template file for multiline context names.
|
|
204
|
+
A multiline context name is specified by using '\n' or ';' as a separator.
|
|
205
|
+
- font: The name of the font to use. The default font is Helvetica.
|
|
186
206
|
- name_y: The Y coordinate of the name on the credential (vertical position on the template).
|
|
187
207
|
- name_color: The color of the name on the credential (hexadecimal color code).
|
|
188
208
|
- name_size: The font size of the name on the credential. The default value is 32.
|
|
189
|
-
-
|
|
209
|
+
- name_font: The font of the name on the credential. It overrides the `font` option.
|
|
210
|
+
- name_uppercase: If set to true (without quotes), the name will be converted to uppercase.
|
|
211
|
+
The default value is False, unless specified otherwise in the instance settings.
|
|
212
|
+
- context_name: Specify the custom course or Learning Path name. If not provided, it will be retrieved
|
|
213
|
+
automatically from the "cert_name_long" or "display_name" fields for courses, or from the Learning Path model.
|
|
190
214
|
- context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
|
|
191
215
|
- context_name_color: The color of the context name on the credential (hexadecimal color code).
|
|
192
216
|
- context_name_size: The font size of the context name on the credential. The default value is 28.
|
|
217
|
+
- context_name_font: The font of the context name on the credential. It overrides the `font` option.
|
|
193
218
|
- issue_date_y: The Y coordinate of the issue date on the credential (vertical position on the template).
|
|
194
219
|
- issue_date_color: The color of the issue date on the credential (hexadecimal color code).
|
|
220
|
+
- issue_date_size: The font size of the issue date on the credential. The default value is 12.
|
|
221
|
+
- issue_date_font: The font of the issue date on the credential. It overrides the `font` option.
|
|
222
|
+
- issue_date_char_space: The character spacing of the issue date on the credential
|
|
223
|
+
(default is 0.0, unless specified otherwise in the instance settings).
|
|
224
|
+
- issue_date_uppercase: If set to true (without quotes), the issue date will be converted to uppercase.
|
|
225
|
+
The default value is False, unless specified otherwise in the instance settings.
|
|
195
226
|
"""
|
|
196
227
|
log.info("Starting credential generation for user %s", user.id)
|
|
197
228
|
|
|
198
229
|
username = _get_user_name(user)
|
|
199
230
|
context_name = options.get('context_name') or get_learning_context_name(learning_context_key)
|
|
231
|
+
template_path = options.get('template')
|
|
200
232
|
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
if '
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
# Handle multiline context name (we support semicolon as a separator to preserve backward compatibility).
|
|
234
|
+
context_name = context_name.replace(';', '\n').replace(r'\n', '\n')
|
|
235
|
+
if '\n' in context_name:
|
|
236
|
+
# `template_two_lines` is kept for backward compatibility.
|
|
237
|
+
template_path = options.get('template_multiline', options.get('template_two_lines', template_path))
|
|
238
|
+
|
|
239
|
+
if not template_path:
|
|
240
|
+
msg = "Template path must be specified in options."
|
|
241
|
+
raise ValueError(msg)
|
|
208
242
|
|
|
209
|
-
|
|
243
|
+
# Get template from the CredentialAsset.
|
|
244
|
+
template_file = CredentialAsset.get_asset_by_slug(template_path)
|
|
210
245
|
|
|
211
246
|
# Load the PDF template.
|
|
212
247
|
with template_file.open('rb') as template_file:
|
|
@@ -215,7 +250,7 @@ def generate_pdf_credential(
|
|
|
215
250
|
credential = PdfWriter()
|
|
216
251
|
|
|
217
252
|
# Create a new canvas, prepare the page and write the data
|
|
218
|
-
pdf_canvas = _write_text_on_template(template,
|
|
253
|
+
pdf_canvas = _write_text_on_template(template, username, context_name, options)
|
|
219
254
|
|
|
220
255
|
overlay_pdf = PdfReader(io.BytesIO(pdf_canvas.getpdfdata()))
|
|
221
256
|
template.merge_page(overlay_pdf.pages[0])
|