learning-credentials 0.4.0rc1__py3-none-any.whl → 0.4.0rc3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- learning_credentials/compat.py +6 -3
- learning_credentials/generators.py +66 -30
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/METADATA +3 -1
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/RECORD +8 -8
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/WHEEL +0 -0
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/entry_points.txt +0 -0
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/licenses/LICENSE.txt +0 -0
- {learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/top_level.txt +0 -0
learning_credentials/compat.py
CHANGED
|
@@ -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:
|
|
@@ -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,19 +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
|
-
|
|
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))
|
|
93
103
|
name_color = options.get('name_color', '#000')
|
|
94
104
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color))
|
|
95
105
|
|
|
96
106
|
name_x = (template_width - pdf_canvas.stringWidth(username)) / 2
|
|
97
107
|
name_y = options.get('name_y', 290)
|
|
108
|
+
|
|
98
109
|
pdf_canvas.drawString(name_x, name_y, username)
|
|
99
110
|
|
|
100
111
|
# Write the learning context name.
|
|
101
|
-
|
|
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))
|
|
102
114
|
context_name_color = options.get('context_name_color', '#000')
|
|
103
115
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(context_name_color))
|
|
104
116
|
|
|
@@ -113,13 +125,22 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
|
|
|
113
125
|
|
|
114
126
|
# Write the issue date.
|
|
115
127
|
issue_date = get_localized_credential_date()
|
|
116
|
-
|
|
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))
|
|
117
133
|
issue_date_color = options.get('issue_date_color', '#000')
|
|
118
134
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color))
|
|
119
135
|
|
|
120
136
|
issue_date_x = (template_width - pdf_canvas.stringWidth(issue_date)) / 2
|
|
121
137
|
issue_date_y = options.get('issue_date_y', 120)
|
|
122
|
-
|
|
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)
|
|
123
144
|
|
|
124
145
|
return pdf_canvas
|
|
125
146
|
|
|
@@ -168,7 +189,7 @@ def generate_pdf_credential(
|
|
|
168
189
|
credential_uuid: UUID,
|
|
169
190
|
options: dict[str, Any],
|
|
170
191
|
) -> str:
|
|
171
|
-
"""
|
|
192
|
+
r"""
|
|
172
193
|
Generate a PDF credential.
|
|
173
194
|
|
|
174
195
|
:param learning_context_key: The ID of the course or learning path the credential is for.
|
|
@@ -179,33 +200,48 @@ def generate_pdf_credential(
|
|
|
179
200
|
|
|
180
201
|
Options:
|
|
181
202
|
- template: The path to the PDF template file.
|
|
182
|
-
-
|
|
183
|
-
A
|
|
184
|
-
- 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.
|
|
185
206
|
- name_y: The Y coordinate of the name on the credential (vertical position on the template).
|
|
186
207
|
- name_color: The color of the name on the credential (hexadecimal color code).
|
|
187
208
|
- name_size: The font size of the name on the credential. The default value is 32.
|
|
188
|
-
-
|
|
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.
|
|
189
214
|
- context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
|
|
190
215
|
- context_name_color: The color of the context name on the credential (hexadecimal color code).
|
|
191
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.
|
|
192
218
|
- issue_date_y: The Y coordinate of the issue date on the credential (vertical position on the template).
|
|
193
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.
|
|
194
226
|
"""
|
|
195
227
|
log.info("Starting credential generation for user %s", user.id)
|
|
196
228
|
|
|
197
229
|
username = _get_user_name(user)
|
|
198
230
|
context_name = options.get('context_name') or get_learning_context_name(learning_context_key)
|
|
231
|
+
template_path = options.get('template')
|
|
199
232
|
|
|
200
|
-
#
|
|
201
|
-
|
|
202
|
-
if '
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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)
|
|
207
242
|
|
|
208
|
-
|
|
243
|
+
# Get template from the CredentialAsset.
|
|
244
|
+
template_file = CredentialAsset.get_asset_by_slug(template_path)
|
|
209
245
|
|
|
210
246
|
# Load the PDF template.
|
|
211
247
|
with template_file.open('rb') as template_file:
|
|
@@ -214,7 +250,7 @@ def generate_pdf_credential(
|
|
|
214
250
|
credential = PdfWriter()
|
|
215
251
|
|
|
216
252
|
# Create a new canvas, prepare the page and write the data
|
|
217
|
-
pdf_canvas = _write_text_on_template(template,
|
|
253
|
+
pdf_canvas = _write_text_on_template(template, username, context_name, options)
|
|
218
254
|
|
|
219
255
|
overlay_pdf = PdfReader(io.BytesIO(pdf_canvas.getpdfdata()))
|
|
220
256
|
template.merge_page(overlay_pdf.pages[0])
|
{learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.0rc3
|
|
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
|
|
@@ -184,6 +184,8 @@ Added
|
|
|
184
184
|
|
|
185
185
|
* Frontend form and backend API endpoint for verifying credentials.
|
|
186
186
|
* Option to invalidate issued credentials.
|
|
187
|
+
* Support for defining the course name using the `cert_name_long` field (in Studio's Advanced Settings).
|
|
188
|
+
* Support for specifying individual fonts for PDF text elements.
|
|
187
189
|
|
|
188
190
|
0.3.0 - 2025-09-17
|
|
189
191
|
******************
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
learning_credentials/__init__.py,sha256=8Q0-3Hdnfmcj41EKu1GSfzEfwWcYNDlItyEEke2r9bs,62
|
|
2
2
|
learning_credentials/admin.py,sha256=gLVpCn5oOHLL3u-wnx4R1yJXfar1Z32vk8zcTVjtBFY,11791
|
|
3
3
|
learning_credentials/apps.py,sha256=trdQxe-JRhUdUaOQoQWiGL1sn6I1sfDiTvdCwy8yGuw,1037
|
|
4
|
-
learning_credentials/compat.py,sha256=
|
|
4
|
+
learning_credentials/compat.py,sha256=bTAB6bTh99ZyhUqOsDtM_BuIPzFxCjySFtfvc-_fCd4,4731
|
|
5
5
|
learning_credentials/exceptions.py,sha256=UaqBVXFMWR2Iob7_LMb3j4NNVmWQFAgLi_MNMRUvGsI,290
|
|
6
|
-
learning_credentials/generators.py,sha256=
|
|
6
|
+
learning_credentials/generators.py,sha256=GhKrBuPSXOkw4k1ejPTYZBJm78m-sou6ffpUNCGLm28,11492
|
|
7
7
|
learning_credentials/models.py,sha256=DncnadwVrmVcbrka9LBmKvTMqLq8os_wHXmWc3Zet6s,17949
|
|
8
8
|
learning_credentials/processors.py,sha256=LkdjmkLBnXc9qeMcksB1T8AQ5ZhYaECyQO__KfHB_aU,15212
|
|
9
9
|
learning_credentials/tasks.py,sha256=byoFEUvN_ayVaU5K5SlEiA7vu9BRPaSSmKnB9g5toec,1927
|
|
@@ -34,9 +34,9 @@ learning_credentials/templates/learning_credentials/edx_ace/certificate_generate
|
|
|
34
34
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt,sha256=-n8tjPSwfwAfeOSZ1WhcCTrpOah4VswzMZ5mh63Pxow,20
|
|
35
35
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt,sha256=S7Hc5T_sZSsSBXm5_H5HBNNv16Ohl0oZn0nVqqeWL0g,132
|
|
37
|
-
learning_credentials-0.4.
|
|
38
|
-
learning_credentials-0.4.
|
|
39
|
-
learning_credentials-0.4.
|
|
40
|
-
learning_credentials-0.4.
|
|
41
|
-
learning_credentials-0.4.
|
|
42
|
-
learning_credentials-0.4.
|
|
37
|
+
learning_credentials-0.4.0rc3.dist-info/licenses/LICENSE.txt,sha256=GDpsPnW_1NKhPvZpZL9imz25P2nIpbwJPEhrlq4vPAU,34523
|
|
38
|
+
learning_credentials-0.4.0rc3.dist-info/METADATA,sha256=nD_YCmP9I0V83xVmX6aBzTSXu_kpYth4kmX1KNjrUVo,7468
|
|
39
|
+
learning_credentials-0.4.0rc3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
40
|
+
learning_credentials-0.4.0rc3.dist-info/entry_points.txt,sha256=hHqqLUEdzAN24v5OGBX9Fr-wh3ATDPjQjByKz03eC2Y,91
|
|
41
|
+
learning_credentials-0.4.0rc3.dist-info/top_level.txt,sha256=Ce-4_leZe_nny7CpmkeRiemcDV6jIHpIvLjlcQBuf18,21
|
|
42
|
+
learning_credentials-0.4.0rc3.dist-info/RECORD,,
|
|
File without changes
|
{learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc1.dist-info → learning_credentials-0.4.0rc3.dist-info}/top_level.txt
RENAMED
|
File without changes
|