learning-credentials 0.4.0rc1__tar.gz → 0.4.0rc2__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.
Files changed (54) hide show
  1. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/CHANGELOG.rst +2 -0
  2. {learning_credentials-0.4.0rc1/learning_credentials.egg-info → learning_credentials-0.4.0rc2}/PKG-INFO +3 -1
  3. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/compat.py +6 -3
  4. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/generators.py +32 -20
  5. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2/learning_credentials.egg-info}/PKG-INFO +3 -1
  6. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/pyproject.toml +1 -1
  7. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/tests/test_generators.py +5 -10
  8. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/LICENSE.txt +0 -0
  9. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/MANIFEST.in +0 -0
  10. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/README.rst +0 -0
  11. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/__init__.py +0 -0
  12. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/admin.py +0 -0
  13. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/__init__.py +0 -0
  14. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/urls.py +0 -0
  15. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/v1/__init__.py +0 -0
  16. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/v1/permissions.py +0 -0
  17. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/v1/serializers.py +0 -0
  18. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/v1/urls.py +0 -0
  19. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/api/v1/views.py +0 -0
  20. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/apps.py +0 -0
  21. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/conf/locale/config.yaml +0 -0
  22. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/exceptions.py +0 -0
  23. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0001_initial.py +0 -0
  24. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
  25. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
  26. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
  27. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
  28. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
  29. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/0007_validation.py +0 -0
  30. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/migrations/__init__.py +0 -0
  31. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/models.py +0 -0
  32. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/processors.py +0 -0
  33. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/settings/__init__.py +0 -0
  34. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/settings/common.py +0 -0
  35. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/settings/production.py +0 -0
  36. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/tasks.py +0 -0
  37. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/base.html +0 -0
  38. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
  39. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
  40. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
  41. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
  42. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
  43. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/templates/learning_credentials/verify.html +0 -0
  44. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials/urls.py +0 -0
  45. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials.egg-info/SOURCES.txt +0 -0
  46. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials.egg-info/dependency_links.txt +0 -0
  47. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials.egg-info/entry_points.txt +0 -0
  48. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials.egg-info/requires.txt +0 -0
  49. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/learning_credentials.egg-info/top_level.txt +0 -0
  50. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/setup.cfg +0 -0
  51. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/tests/test_models.py +0 -0
  52. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/tests/test_processors.py +0 -0
  53. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/tests/test_tasks.py +0 -0
  54. {learning_credentials-0.4.0rc1 → learning_credentials-0.4.0rc2}/tests/test_views.py +0 -0
@@ -24,6 +24,8 @@ Added
24
24
 
25
25
  * Frontend form and backend API endpoint for verifying credentials.
26
26
  * Option to invalidate issued credentials.
27
+ * Support for defining the course name using the `cert_name_long` field (in Studio's Advanced Settings).
28
+ * Support for specifying individual fonts for PDF text elements.
27
29
 
28
30
  0.3.0 - 2025-09-17
29
31
  ******************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.4.0rc1
3
+ Version: 0.4.0rc2
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
  ******************
@@ -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.learning_sequences.api import get_course_outline
55
+ from openedx.core.djangoapps.content.course_overviews.api import get_course_overview_or_none
56
56
 
57
- course_outline = get_course_outline(course_id)
58
- return (course_outline and course_outline.title) or str(course_id)
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 pdfmetrics
23
- from reportlab.pdfbase.ttfonts import TTFont
24
- from reportlab.pdfgen import canvas
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(options: dict[str, Any]) -> str:
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 options: A dictionary containing the font.
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 font := options.get('font'):
56
- pdfmetrics.registerFont(TTFont(font, CredentialAsset.get_asset_by_slug(font)))
56
+ if not font_name:
57
+ return None
57
58
 
58
- return font or 'Helvetica'
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: any, font: str, username: str, context_name: str, options: dict[str, Any]) -> any:
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,10 +91,12 @@ 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 = canvas.Canvas(io.BytesIO(), pagesize=(template_width, template_height))
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
- pdf_canvas.setFont(font, options.get('name_size', 32))
98
+ name_font = _register_font(options.get('name_font')) or font
99
+ pdf_canvas.setFont(name_font, options.get('name_size', 32))
93
100
  name_color = options.get('name_color', '#000')
94
101
  pdf_canvas.setFillColorRGB(*hex_to_rgb(name_color))
95
102
 
@@ -98,7 +105,8 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
98
105
  pdf_canvas.drawString(name_x, name_y, username)
99
106
 
100
107
  # Write the learning context name.
101
- pdf_canvas.setFont(font, options.get('context_name_size', 28))
108
+ context_name_font = _register_font(options.get('context_name_font')) or font
109
+ pdf_canvas.setFont(context_name_font, options.get('context_name_size', 28))
102
110
  context_name_color = options.get('context_name_color', '#000')
103
111
  pdf_canvas.setFillColorRGB(*hex_to_rgb(context_name_color))
104
112
 
@@ -113,7 +121,8 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
113
121
 
114
122
  # Write the issue date.
115
123
  issue_date = get_localized_credential_date()
116
- pdf_canvas.setFont(font, 12)
124
+ issue_date_font = _register_font(options.get('issue_date_font')) or font
125
+ pdf_canvas.setFont(issue_date_font, options.get('issue_date_size', 12))
117
126
  issue_date_color = options.get('issue_date_color', '#000')
118
127
  pdf_canvas.setFillColorRGB(*hex_to_rgb(issue_date_color))
119
128
 
@@ -181,16 +190,21 @@ def generate_pdf_credential(
181
190
  - template: The path to the PDF template file.
182
191
  - template_two_lines: The path to the PDF template file for two-line context names.
183
192
  A two-line context name is specified by using a semicolon as a separator.
184
- - font: The name of the font to use.
193
+ - font: The name of the font to use. The default font is Helvetica.
185
194
  - name_y: The Y coordinate of the name on the credential (vertical position on the template).
186
195
  - name_color: The color of the name on the credential (hexadecimal color code).
187
196
  - name_size: The font size of the name on the credential. The default value is 32.
188
- - context_name: Specify the custom course or Learning Path name.
197
+ - name_font: The font of the name on the credential. It overrides the `font` option.
198
+ - context_name: Specify the custom course or Learning Path name. If not provided, it will be retrieved
199
+ automatically from the "cert_name_long" or "display_name" fields for courses, or from the Learning Path model.
189
200
  - context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
190
201
  - context_name_color: The color of the context name on the credential (hexadecimal color code).
191
202
  - context_name_size: The font size of the context name on the credential. The default value is 28.
203
+ - context_name_font: The font of the context name on the credential. It overrides the `font` option.
192
204
  - issue_date_y: The Y coordinate of the issue date on the credential (vertical position on the template).
193
205
  - issue_date_color: The color of the issue date on the credential (hexadecimal color code).
206
+ - issue_date_size: The font size of the issue date on the credential. The default value is 12.
207
+ - issue_date_font: The font of the issue date on the credential. It overrides the `font` option.
194
208
  """
195
209
  log.info("Starting credential generation for user %s", user.id)
196
210
 
@@ -205,8 +219,6 @@ def generate_pdf_credential(
205
219
  else:
206
220
  template_file = CredentialAsset.get_asset_by_slug(options['template'])
207
221
 
208
- font = _register_font(options)
209
-
210
222
  # Load the PDF template.
211
223
  with template_file.open('rb') as template_file:
212
224
  template = PdfReader(template_file).pages[0]
@@ -214,7 +226,7 @@ def generate_pdf_credential(
214
226
  credential = PdfWriter()
215
227
 
216
228
  # Create a new canvas, prepare the page and write the data
217
- pdf_canvas = _write_text_on_template(template, font, username, context_name, options)
229
+ pdf_canvas = _write_text_on_template(template, username, context_name, options)
218
230
 
219
231
  overlay_pdf = PdfReader(io.BytesIO(pdf_canvas.getpdfdata()))
220
232
  template.merge_page(overlay_pdf.pages[0])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.4.0rc1
3
+ Version: 0.4.0rc2
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning-credentials"
3
- version = "0.4.0-rc1"
3
+ version = "0.4.0-rc2"
4
4
  description = "A pluggable service for preparing Open edX credentials."
5
5
  dynamic = ["readme"]
6
6
  requires-python = ">=3.11"
@@ -42,21 +42,19 @@ def test_get_user_name():
42
42
  def test_register_font_without_custom_font(mock_get_asset_by_slug: Mock):
43
43
  """Test the _register_font falls back to the default font when no custom font is specified."""
44
44
  options = {}
45
- assert _register_font(options) == "Helvetica"
45
+ assert _register_font(options) is None
46
46
  mock_get_asset_by_slug.assert_not_called()
47
47
 
48
48
 
49
49
  @patch("learning_credentials.generators.CredentialAsset.get_asset_by_slug")
50
50
  @patch('learning_credentials.generators.TTFont')
51
- @patch("learning_credentials.generators.pdfmetrics.registerFont")
51
+ @patch("learning_credentials.generators.registerFont")
52
52
  def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_class: Mock, mock_get_asset_by_slug: Mock):
53
53
  """Test the _register_font registers the custom font when specified."""
54
54
  custom_font = "MyFont"
55
- options = {"font": custom_font}
56
-
57
55
  mock_get_asset_by_slug.return_value = "font_path"
58
56
 
59
- assert _register_font(options) == custom_font
57
+ assert _register_font(custom_font) == custom_font
60
58
  mock_get_asset_by_slug.assert_called_once_with(custom_font)
61
59
  mock_font_class.assert_called_once_with(custom_font, mock_get_asset_by_slug.return_value)
62
60
  mock_register_font.assert_called_once_with(mock_font_class.return_value)
@@ -87,7 +85,7 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas
87
85
  ('Programming\n101\nAdvanced Programming', {}, {}), # Multiline course name.
88
86
  ],
89
87
  )
90
- @patch('learning_credentials.generators.canvas.Canvas', return_value=Mock(stringWidth=Mock(return_value=10)))
88
+ @patch('learning_credentials.generators.Canvas', return_value=Mock(stringWidth=Mock(return_value=10)))
91
89
  def test_write_text_on_template(mock_canvas_class: Mock, context_name: str, options: dict[str, int], expected: dict):
92
90
  """Test the _write_text_on_template function."""
93
91
  username = 'John Doe'
@@ -106,7 +104,7 @@ def test_write_text_on_template(mock_canvas_class: Mock, context_name: str, opti
106
104
 
107
105
  # Call the function with test parameters and mocks
108
106
  with patch('learning_credentials.generators.get_localized_credential_date', return_value=test_date):
109
- _write_text_on_template(template_mock, font, username, context_name, options)
107
+ _write_text_on_template(template_mock, username, context_name, options)
110
108
 
111
109
  # Verifying that Canvas was the correct pagesize.
112
110
  # Use `call_args_list` to ignore the first argument, which is an instance of io.BytesIO.
@@ -251,7 +249,6 @@ def test_save_credential(mock_contentfile: Mock, mock_token_hex: Mock, storage:
251
249
  )
252
250
  @patch('learning_credentials.generators._get_user_name')
253
251
  @patch('learning_credentials.generators.get_learning_context_name')
254
- @patch('learning_credentials.generators._register_font')
255
252
  @patch('learning_credentials.generators.PdfReader')
256
253
  @patch('learning_credentials.generators.PdfWriter')
257
254
  @patch(
@@ -264,7 +261,6 @@ def test_generate_pdf_credential(
264
261
  mock_write_text_on_template: Mock,
265
262
  mock_pdf_writer: Mock,
266
263
  mock_pdf_reader: Mock,
267
- mock_register_font: Mock,
268
264
  mock_get_learning_context_name: Mock,
269
265
  mock_get_user_name: Mock,
270
266
  mock_get_asset_by_slug: Mock,
@@ -287,7 +283,6 @@ def test_generate_pdf_credential(
287
283
  mock_get_learning_context_name.assert_not_called()
288
284
  else:
289
285
  mock_get_learning_context_name.assert_called_once_with(course_id)
290
- mock_register_font.assert_called_once_with(options)
291
286
  assert mock_pdf_reader.call_count == 2
292
287
  mock_pdf_writer.assert_called_once_with()
293
288