learning-credentials 0.4.0rc2__tar.gz → 0.4.0rc3__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.4.0rc2/learning_credentials.egg-info → learning_credentials-0.4.0rc3}/PKG-INFO +1 -1
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/generators.py +34 -10
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3/learning_credentials.egg-info}/PKG-INFO +1 -1
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/pyproject.toml +2 -1
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/tests/test_generators.py +128 -12
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/CHANGELOG.rst +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/LICENSE.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/MANIFEST.in +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/README.rst +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/__init__.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/admin.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/__init__.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/urls.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/__init__.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/permissions.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/serializers.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/urls.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/views.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/apps.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/compat.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/conf/locale/config.yaml +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/exceptions.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0001_initial.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/0007_validation.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/migrations/__init__.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/models.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/processors.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/settings/__init__.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/settings/common.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/settings/production.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/tasks.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/base.html +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/templates/learning_credentials/verify.html +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/urls.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials.egg-info/SOURCES.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials.egg-info/dependency_links.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials.egg-info/entry_points.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials.egg-info/requires.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials.egg-info/top_level.txt +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/setup.cfg +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/tests/test_models.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/tests/test_processors.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/tests/test_tasks.py +0 -0
- {learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/tests/test_views.py +0 -0
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/generators.py
RENAMED
|
@@ -95,6 +95,9 @@ def _write_text_on_template(template: PageObject, username: str, context_name: s
|
|
|
95
95
|
font = _register_font(options.get('font')) or 'Helvetica'
|
|
96
96
|
|
|
97
97
|
# Write the learner name.
|
|
98
|
+
if options.get('name_uppercase', getattr(settings, 'LEARNING_CREDENTIALS_NAME_UPPERCASE', False)):
|
|
99
|
+
username = username.upper()
|
|
100
|
+
|
|
98
101
|
name_font = _register_font(options.get('name_font')) or font
|
|
99
102
|
pdf_canvas.setFont(name_font, options.get('name_size', 32))
|
|
100
103
|
name_color = options.get('name_color', '#000')
|
|
@@ -102,6 +105,7 @@ def _write_text_on_template(template: PageObject, username: str, context_name: s
|
|
|
102
105
|
|
|
103
106
|
name_x = (template_width - pdf_canvas.stringWidth(username)) / 2
|
|
104
107
|
name_y = options.get('name_y', 290)
|
|
108
|
+
|
|
105
109
|
pdf_canvas.drawString(name_x, name_y, username)
|
|
106
110
|
|
|
107
111
|
# Write the learning context name.
|
|
@@ -121,6 +125,9 @@ def _write_text_on_template(template: PageObject, username: str, context_name: s
|
|
|
121
125
|
|
|
122
126
|
# Write the issue date.
|
|
123
127
|
issue_date = get_localized_credential_date()
|
|
128
|
+
if options.get('issue_date_uppercase', getattr(settings, 'LEARNING_CREDENTIALS_ISSUE_DATE_UPPERCASE', False)):
|
|
129
|
+
issue_date = issue_date.upper()
|
|
130
|
+
|
|
124
131
|
issue_date_font = _register_font(options.get('issue_date_font')) or font
|
|
125
132
|
pdf_canvas.setFont(issue_date_font, options.get('issue_date_size', 12))
|
|
126
133
|
issue_date_color = options.get('issue_date_color', '#000')
|
|
@@ -128,7 +135,12 @@ def _write_text_on_template(template: PageObject, username: str, context_name: s
|
|
|
128
135
|
|
|
129
136
|
issue_date_x = (template_width - pdf_canvas.stringWidth(issue_date)) / 2
|
|
130
137
|
issue_date_y = options.get('issue_date_y', 120)
|
|
131
|
-
|
|
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)
|
|
132
144
|
|
|
133
145
|
return pdf_canvas
|
|
134
146
|
|
|
@@ -177,7 +189,7 @@ def generate_pdf_credential(
|
|
|
177
189
|
credential_uuid: UUID,
|
|
178
190
|
options: dict[str, Any],
|
|
179
191
|
) -> str:
|
|
180
|
-
"""
|
|
192
|
+
r"""
|
|
181
193
|
Generate a PDF credential.
|
|
182
194
|
|
|
183
195
|
:param learning_context_key: The ID of the course or learning path the credential is for.
|
|
@@ -188,13 +200,15 @@ def generate_pdf_credential(
|
|
|
188
200
|
|
|
189
201
|
Options:
|
|
190
202
|
- template: The path to the PDF template file.
|
|
191
|
-
-
|
|
192
|
-
A
|
|
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.
|
|
193
205
|
- font: The name of the font to use. The default font is Helvetica.
|
|
194
206
|
- name_y: The Y coordinate of the name on the credential (vertical position on the template).
|
|
195
207
|
- name_color: The color of the name on the credential (hexadecimal color code).
|
|
196
208
|
- name_size: The font size of the name on the credential. The default value is 32.
|
|
197
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.
|
|
198
212
|
- context_name: Specify the custom course or Learning Path name. If not provided, it will be retrieved
|
|
199
213
|
automatically from the "cert_name_long" or "display_name" fields for courses, or from the Learning Path model.
|
|
200
214
|
- context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
|
|
@@ -205,19 +219,29 @@ def generate_pdf_credential(
|
|
|
205
219
|
- issue_date_color: The color of the issue date on the credential (hexadecimal color code).
|
|
206
220
|
- issue_date_size: The font size of the issue date on the credential. The default value is 12.
|
|
207
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.
|
|
208
226
|
"""
|
|
209
227
|
log.info("Starting credential generation for user %s", user.id)
|
|
210
228
|
|
|
211
229
|
username = _get_user_name(user)
|
|
212
230
|
context_name = options.get('context_name') or get_learning_context_name(learning_context_key)
|
|
231
|
+
template_path = options.get('template')
|
|
232
|
+
|
|
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)
|
|
213
242
|
|
|
214
243
|
# Get template from the CredentialAsset.
|
|
215
|
-
|
|
216
|
-
if ';' in context_name and (template_path := options.get('template_two_lines')):
|
|
217
|
-
template_file = CredentialAsset.get_asset_by_slug(template_path)
|
|
218
|
-
context_name = context_name.replace(';', '\n')
|
|
219
|
-
else:
|
|
220
|
-
template_file = CredentialAsset.get_asset_by_slug(options['template'])
|
|
244
|
+
template_file = CredentialAsset.get_asset_by_slug(template_path)
|
|
221
245
|
|
|
222
246
|
# Load the PDF template.
|
|
223
247
|
with template_file.open('rb') as template_file:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "learning-credentials"
|
|
3
|
-
version = "0.4.0-
|
|
3
|
+
version = "0.4.0-rc3"
|
|
4
4
|
description = "A pluggable service for preparing Open edX credentials."
|
|
5
5
|
dynamic = ["readme"]
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -188,6 +188,7 @@ ignore = [
|
|
|
188
188
|
'RUF018', # assignment-in-assert
|
|
189
189
|
'ARG002', # unused-method-argument
|
|
190
190
|
'PLR0913', # too-many-arguments
|
|
191
|
+
'FBT001', # flake8-boolean-trap
|
|
191
192
|
]
|
|
192
193
|
|
|
193
194
|
[tool.ruff.lint.flake8-annotations]
|
|
@@ -17,6 +17,7 @@ from pypdf import PdfWriter
|
|
|
17
17
|
from pypdf.constants import UserAccessPermissions
|
|
18
18
|
|
|
19
19
|
from learning_credentials.generators import (
|
|
20
|
+
FontError,
|
|
20
21
|
_get_user_name,
|
|
21
22
|
_register_font,
|
|
22
23
|
_save_credential,
|
|
@@ -59,6 +60,21 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas
|
|
|
59
60
|
mock_font_class.assert_called_once_with(custom_font, mock_get_asset_by_slug.return_value)
|
|
60
61
|
mock_register_font.assert_called_once_with(mock_font_class.return_value)
|
|
61
62
|
|
|
63
|
+
@patch("learning_credentials.generators.CredentialAsset.get_asset_by_slug")
|
|
64
|
+
@patch('learning_credentials.generators.TTFont', side_effect=FontError("Font registration failed"))
|
|
65
|
+
@patch("learning_credentials.generators.registerFont")
|
|
66
|
+
def test_register_font_with_registration_failure(
|
|
67
|
+
mock_register_font: Mock, mock_font_class: Mock, mock_get_asset_by_slug: Mock
|
|
68
|
+
):
|
|
69
|
+
"""Test the _register_font returns None when font registration fails."""
|
|
70
|
+
custom_font = "MyFont"
|
|
71
|
+
mock_get_asset_by_slug.return_value = "font_path"
|
|
72
|
+
|
|
73
|
+
assert _register_font(custom_font) is None
|
|
74
|
+
mock_get_asset_by_slug.assert_called_once_with(custom_font)
|
|
75
|
+
mock_font_class.assert_called_once_with(custom_font, mock_get_asset_by_slug.return_value)
|
|
76
|
+
mock_register_font.assert_not_called()
|
|
77
|
+
|
|
62
78
|
|
|
63
79
|
@pytest.mark.parametrize(
|
|
64
80
|
("context_name", "options", "expected"),
|
|
@@ -153,6 +169,90 @@ def test_write_text_on_template(mock_canvas_class: Mock, context_name: str, opti
|
|
|
153
169
|
assert canvas_object.drawString.mock_calls[-1][1] == (expected_issue_date_x, expected_issue_date_y, test_date)
|
|
154
170
|
|
|
155
171
|
|
|
172
|
+
@pytest.mark.parametrize(
|
|
173
|
+
("option_value", "setting_value", "expected_uppercase"),
|
|
174
|
+
[
|
|
175
|
+
(None, False, False), # No option set, setting is False - use default (lowercase)
|
|
176
|
+
(None, True, True), # No option set, setting is True - use setting (uppercase)
|
|
177
|
+
(False, False, False), # Explicitly disabled, setting is False
|
|
178
|
+
(False, True, False), # Explicitly disabled, setting is True - option overrides
|
|
179
|
+
(True, False, True), # Explicitly enabled, setting is False - option overrides
|
|
180
|
+
(True, True, True), # Explicitly enabled, setting is True
|
|
181
|
+
],
|
|
182
|
+
)
|
|
183
|
+
@patch('learning_credentials.generators.get_localized_credential_date', return_value='April 1, 2021')
|
|
184
|
+
@patch('learning_credentials.generators.Canvas', return_value=Mock(stringWidth=Mock(return_value=10)))
|
|
185
|
+
def test_write_text_on_template_uppercase(
|
|
186
|
+
mock_canvas_class: Mock,
|
|
187
|
+
mock_get_date: Mock,
|
|
188
|
+
option_value: bool | None,
|
|
189
|
+
setting_value: bool,
|
|
190
|
+
expected_uppercase: bool,
|
|
191
|
+
):
|
|
192
|
+
"""Test the _write_text_on_template function with uppercase option and settings."""
|
|
193
|
+
username = "John Doe"
|
|
194
|
+
context_name = "Programming 101"
|
|
195
|
+
template_mock = Mock(mediabox=[0, 0, 300, 200])
|
|
196
|
+
options = {}
|
|
197
|
+
|
|
198
|
+
if expected_uppercase:
|
|
199
|
+
expected_name = "JOHN DOE"
|
|
200
|
+
expected_date = "APRIL 1, 2021"
|
|
201
|
+
else:
|
|
202
|
+
expected_name = username
|
|
203
|
+
expected_date = mock_get_date.return_value
|
|
204
|
+
|
|
205
|
+
if option_value is not None:
|
|
206
|
+
options['name_uppercase'] = option_value
|
|
207
|
+
options['issue_date_uppercase'] = option_value
|
|
208
|
+
|
|
209
|
+
with override_settings(
|
|
210
|
+
LEARNING_CREDENTIALS_NAME_UPPERCASE=setting_value,
|
|
211
|
+
LEARNING_CREDENTIALS_ISSUE_DATE_UPPERCASE=setting_value,
|
|
212
|
+
):
|
|
213
|
+
_write_text_on_template(template_mock, username, context_name, options)
|
|
214
|
+
|
|
215
|
+
assert mock_canvas_class.return_value.drawString.mock_calls[-3][1][2] == expected_name
|
|
216
|
+
assert mock_canvas_class.return_value.drawString.mock_calls[-1][1][2] == expected_date
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@pytest.mark.parametrize(
|
|
220
|
+
("option_value", "setting_value", "expected_char_space"),
|
|
221
|
+
[
|
|
222
|
+
(None, 0, 0), # No option set, setting is 0 - use default
|
|
223
|
+
(None, 2, 2), # No option set, setting has value - use setting
|
|
224
|
+
(0, 0, 0), # Explicitly set to 0, setting is 0
|
|
225
|
+
(0, 2, 0), # Explicitly set to 0, setting has value - option overrides
|
|
226
|
+
(3, 0, 3), # Explicitly set to 3, setting is 0 - option overrides
|
|
227
|
+
(3, 2, 3), # Explicitly set to 3, setting has value - option overrides
|
|
228
|
+
],
|
|
229
|
+
)
|
|
230
|
+
@patch('learning_credentials.generators.get_localized_credential_date', return_value='April 1, 2021')
|
|
231
|
+
@patch('learning_credentials.generators.Canvas', return_value=Mock(stringWidth=Mock(return_value=10)))
|
|
232
|
+
def test_write_text_on_template_issue_date_char_space(
|
|
233
|
+
mock_canvas_class: Mock,
|
|
234
|
+
_mock_get_date: Mock, # noqa: PT019
|
|
235
|
+
option_value: int | None,
|
|
236
|
+
setting_value: int,
|
|
237
|
+
expected_char_space: int,
|
|
238
|
+
):
|
|
239
|
+
"""Test the _write_text_on_template function with issue_date_char_space option and settings."""
|
|
240
|
+
username = "John Doe"
|
|
241
|
+
context_name = "Programming 101"
|
|
242
|
+
template_mock = Mock(mediabox=[0, 0, 300, 200])
|
|
243
|
+
options = {}
|
|
244
|
+
|
|
245
|
+
if option_value is not None:
|
|
246
|
+
options['issue_date_char_space'] = option_value
|
|
247
|
+
|
|
248
|
+
with override_settings(LEARNING_CREDENTIALS_ISSUE_DATE_CHAR_SPACE=setting_value):
|
|
249
|
+
_write_text_on_template(template_mock, username, context_name, options)
|
|
250
|
+
|
|
251
|
+
# Check that the last drawString call (for issue date) has the correct charSpace parameter
|
|
252
|
+
last_draw_call = mock_canvas_class.return_value.drawString.mock_calls[-1]
|
|
253
|
+
assert last_draw_call[2]['charSpace'] == expected_char_space
|
|
254
|
+
|
|
255
|
+
|
|
156
256
|
@override_settings(LMS_ROOT_URL="https://example.com", MEDIA_URL="media/")
|
|
157
257
|
@pytest.mark.parametrize(
|
|
158
258
|
"storage",
|
|
@@ -220,20 +320,22 @@ def test_save_credential(mock_contentfile: Mock, mock_token_hex: Mock, storage:
|
|
|
220
320
|
("context_name", "options", "expected_template_slug", "expected_context_name"),
|
|
221
321
|
[
|
|
222
322
|
# Default.
|
|
223
|
-
('Test Course', {'template': '
|
|
224
|
-
# Specify a different template for two-line course names and replace semicolon with newline in course name.
|
|
225
|
-
(
|
|
226
|
-
'Test Course; Test Course',
|
|
227
|
-
{'template': 'template_slug', 'template_two_lines': 'template_two_lines_slug'},
|
|
228
|
-
'template_two_lines_slug',
|
|
229
|
-
'Test Course\n Test Course',
|
|
230
|
-
),
|
|
231
|
-
# Do not replace semicolon with newline when the `template_two_lines` option is not specified.
|
|
232
|
-
('Test Course; Test Course', {'template': 'template_slug'}, 'template_slug', 'Test Course; Test Course'),
|
|
323
|
+
('Test Course', {'template': 'default'}, 'default', 'Test Course'),
|
|
233
324
|
# Override course name.
|
|
234
|
-
('Test Course', {'template': '
|
|
325
|
+
('Test Course', {'template': 'default', 'context_name': 'Override'}, 'default', 'Override'),
|
|
235
326
|
# Ignore empty course name override.
|
|
236
|
-
('Test Course', {'template': '
|
|
327
|
+
('Test Course', {'template': 'default', 'context_name': ''}, 'default', 'Test Course'),
|
|
328
|
+
# Specify a different template for multiline course names and replace \n with newline.
|
|
329
|
+
('Test\nCourse', {'template': 'default', 'template_multiline': 'multiline'}, 'multiline', 'Test\nCourse'),
|
|
330
|
+
('Test\\nCourse', {'template_multiline': 'multiline'}, 'multiline', 'Test\nCourse'),
|
|
331
|
+
# Backward compatibility with semicolon separator.
|
|
332
|
+
('Test;Course', {'template_multiline': 'multiline'}, 'multiline', 'Test\nCourse'),
|
|
333
|
+
# Mixed semicolon and newline separators.
|
|
334
|
+
('Te\nst\\nCourse;', {'template_multiline': 'multiline'}, 'multiline', 'Te\nst\nCourse\n'),
|
|
335
|
+
# Check backward compatibility with `template_two_lines` option.
|
|
336
|
+
('Test\\nCourse', {'template': 'default', 'template_two_lines': 'two_lines'}, 'two_lines', 'Test\nCourse'),
|
|
337
|
+
# Ensure that the default template is used when no multiline template is specified.
|
|
338
|
+
('Test\\nCourse', {'template': 'default'}, 'default', 'Test\nCourse'),
|
|
237
339
|
],
|
|
238
340
|
)
|
|
239
341
|
@patch(
|
|
@@ -292,3 +394,17 @@ def test_generate_pdf_credential(
|
|
|
292
394
|
assert args[-1] == options
|
|
293
395
|
|
|
294
396
|
mock_save_credential.assert_called_once()
|
|
397
|
+
|
|
398
|
+
@patch('learning_credentials.generators.get_learning_context_name')
|
|
399
|
+
@patch('learning_credentials.generators._get_user_name')
|
|
400
|
+
def test_generate_pdf_credential_no_template(mock_get_user_name: Mock, mock_get_learning_context_name: Mock):
|
|
401
|
+
"""Test that generate_pdf_credential raises ValueError when no template is specified."""
|
|
402
|
+
course_id = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
|
|
403
|
+
user = Mock()
|
|
404
|
+
options = {} # No template specified.
|
|
405
|
+
|
|
406
|
+
with pytest.raises(ValueError, match=r"Template path must be specified in options."):
|
|
407
|
+
generate_pdf_credential(course_id, user, Mock(), options)
|
|
408
|
+
|
|
409
|
+
mock_get_user_name.assert_called_once_with(user)
|
|
410
|
+
mock_get_learning_context_name.assert_called_once_with(course_id)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/__init__.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/admin.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/__init__.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/urls.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/api/v1/views.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/apps.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/compat.py
RENAMED
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/models.py
RENAMED
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/processors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.4.0rc2 → learning_credentials-0.4.0rc3}/learning_credentials/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|