learning-credentials 0.2.0rc3__tar.gz → 0.2.2rc1__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.2.0rc3 → learning_credentials-0.2.2rc1}/CHANGELOG.rst +22 -2
- {learning_credentials-0.2.0rc3/learning_credentials.egg-info → learning_credentials-0.2.2rc1}/PKG-INFO +26 -6
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/README.rst +2 -2
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/__init__.py +1 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/compat.py +2 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/generators.py +2 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0001_initial.py +2 -2
- learning_credentials-0.2.2rc1/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +40 -0
- learning_credentials-0.2.2rc1/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +21 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/models.py +3 -3
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/processors.py +77 -6
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/settings/common.py +0 -3
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1/learning_credentials.egg-info}/PKG-INFO +26 -6
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/SOURCES.txt +1 -5
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/entry_points.txt +0 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/requires.txt +1 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/top_level.txt +0 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/pyproject.toml +3 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/requirements/base.in +1 -2
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/requirements/constraints.txt +4 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/setup.py +1 -2
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/tests/test_generators.py +2 -1
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/tests/test_processors.py +67 -43
- learning_credentials-0.2.0rc3/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -32
- learning_credentials-0.2.0rc3/openedx_certificates/__init__.py +0 -1
- learning_credentials-0.2.0rc3/openedx_certificates/apps.py +0 -11
- learning_credentials-0.2.0rc3/openedx_certificates/migrations/0001_initial.py +0 -206
- learning_credentials-0.2.0rc3/openedx_certificates/migrations/__init__.py +0 -0
- learning_credentials-0.2.0rc3/openedx_certificates/models.py +0 -38
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/LICENSE.txt +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/MANIFEST.in +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/admin.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/apps.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/exceptions.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/__init__.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/settings/__init__.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/settings/production.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/tasks.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/base.html +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/urls.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/views.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/dependency_links.txt +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/not-zip-safe +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/setup.cfg +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/tests/test_models.py +0 -0
- {learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/tests/test_tasks.py +0 -0
|
@@ -16,13 +16,33 @@ Unreleased
|
|
|
16
16
|
|
|
17
17
|
*
|
|
18
18
|
|
|
19
|
-
0.2.
|
|
19
|
+
0.2.2 - 2025-08-05
|
|
20
|
+
|
|
21
|
+
Added
|
|
22
|
+
=====
|
|
23
|
+
|
|
24
|
+
* Step-specific options support for Learning Path credentials.
|
|
25
|
+
|
|
26
|
+
Removed
|
|
27
|
+
=======
|
|
28
|
+
|
|
29
|
+
* Legacy `openedx_certificates` app.
|
|
30
|
+
|
|
31
|
+
0.2.1 – 2025-05-05
|
|
32
|
+
******************
|
|
33
|
+
|
|
34
|
+
Fixed
|
|
35
|
+
=====
|
|
36
|
+
|
|
37
|
+
* Check enrollment status before issuing Learning Path credentials.
|
|
38
|
+
|
|
39
|
+
0.2.0 – 2025-04-03
|
|
20
40
|
******************
|
|
21
41
|
|
|
22
42
|
Added
|
|
23
43
|
=====
|
|
24
44
|
|
|
25
|
-
*
|
|
45
|
+
* Learning Paths support.
|
|
26
46
|
|
|
27
47
|
|
|
28
48
|
0.1.0 – 2025-01-29
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2rc1
|
|
4
4
|
Summary: A pluggable service for preparing Open edX credentials.
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-credentials
|
|
6
6
|
Author: OpenCraft
|
|
@@ -28,7 +28,7 @@ Requires-Dist: django_reverse_admin
|
|
|
28
28
|
Requires-Dist: djangorestframework
|
|
29
29
|
Requires-Dist: edx-opaque-keys
|
|
30
30
|
Requires-Dist: edx_ace
|
|
31
|
-
Requires-Dist: learning-paths-plugin
|
|
31
|
+
Requires-Dist: learning-paths-plugin>=0.3.4
|
|
32
32
|
Requires-Dist: openedx-completion-aggregator
|
|
33
33
|
Requires-Dist: pypdf
|
|
34
34
|
Requires-Dist: reportlab
|
|
@@ -139,7 +139,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
139
139
|
:target: https://pypi.python.org/pypi/learning-credentials/
|
|
140
140
|
:alt: PyPI
|
|
141
141
|
|
|
142
|
-
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/workflows/
|
|
142
|
+
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/actions/workflows/ci.yml/badge.svg?branch=main
|
|
143
143
|
:target: https://github.com/open-craft/learning-credentials/actions
|
|
144
144
|
:alt: CI
|
|
145
145
|
|
|
@@ -159,7 +159,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
159
159
|
:target: https://github.com/open-craft/learning-credentials/blob/main/LICENSE.txt
|
|
160
160
|
:alt: License
|
|
161
161
|
|
|
162
|
-
.. |status-badge| image:: https://img.shields.io/badge/Status-
|
|
162
|
+
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
|
163
163
|
:alt: Status
|
|
164
164
|
|
|
165
165
|
.. https://githubnext.com/projects/repo-visualization/
|
|
@@ -186,13 +186,33 @@ Unreleased
|
|
|
186
186
|
|
|
187
187
|
*
|
|
188
188
|
|
|
189
|
-
0.2.
|
|
189
|
+
0.2.2 - 2025-08-05
|
|
190
|
+
|
|
191
|
+
Added
|
|
192
|
+
=====
|
|
193
|
+
|
|
194
|
+
* Step-specific options support for Learning Path credentials.
|
|
195
|
+
|
|
196
|
+
Removed
|
|
197
|
+
=======
|
|
198
|
+
|
|
199
|
+
* Legacy `openedx_certificates` app.
|
|
200
|
+
|
|
201
|
+
0.2.1 – 2025-05-05
|
|
202
|
+
******************
|
|
203
|
+
|
|
204
|
+
Fixed
|
|
205
|
+
=====
|
|
206
|
+
|
|
207
|
+
* Check enrollment status before issuing Learning Path credentials.
|
|
208
|
+
|
|
209
|
+
0.2.0 – 2025-04-03
|
|
190
210
|
******************
|
|
191
211
|
|
|
192
212
|
Added
|
|
193
213
|
=====
|
|
194
214
|
|
|
195
|
-
*
|
|
215
|
+
* Learning Paths support.
|
|
196
216
|
|
|
197
217
|
|
|
198
218
|
0.1.0 – 2025-01-29
|
|
@@ -92,7 +92,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
92
92
|
:target: https://pypi.python.org/pypi/learning-credentials/
|
|
93
93
|
:alt: PyPI
|
|
94
94
|
|
|
95
|
-
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/workflows/
|
|
95
|
+
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/actions/workflows/ci.yml/badge.svg?branch=main
|
|
96
96
|
:target: https://github.com/open-craft/learning-credentials/actions
|
|
97
97
|
:alt: CI
|
|
98
98
|
|
|
@@ -112,7 +112,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
112
112
|
:target: https://github.com/open-craft/learning-credentials/blob/main/LICENSE.txt
|
|
113
113
|
:alt: License
|
|
114
114
|
|
|
115
|
-
.. |status-badge| image:: https://img.shields.io/badge/Status-
|
|
115
|
+
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
|
116
116
|
:alt: Status
|
|
117
117
|
|
|
118
118
|
.. https://githubnext.com/projects/repo-visualization/
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/compat.py
RENAMED
|
@@ -5,6 +5,8 @@ This module moderates access to all edx-platform features allowing for cross-ver
|
|
|
5
5
|
It also simplifies running tests outside edx-platform's environment by stubbing these functions in unit tests.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
# ruff: noqa: PLC0415
|
|
9
|
+
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
10
12
|
from contextlib import contextmanager
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/generators.py
RENAMED
|
@@ -98,7 +98,7 @@ def _write_text_on_template(template: any, font: str, username: str, context_nam
|
|
|
98
98
|
pdf_canvas.drawString(name_x, name_y, username)
|
|
99
99
|
|
|
100
100
|
# Write the learning context name.
|
|
101
|
-
pdf_canvas.setFont(font, 28)
|
|
101
|
+
pdf_canvas.setFont(font, options.get('context_name_size', 28))
|
|
102
102
|
context_name_color = options.get('context_name_color', '#000')
|
|
103
103
|
pdf_canvas.setFillColorRGB(*hex_to_rgb(context_name_color))
|
|
104
104
|
|
|
@@ -187,6 +187,7 @@ def generate_pdf_credential(
|
|
|
187
187
|
- context_name: Specify the custom course or Learning Path name.
|
|
188
188
|
- context_name_y: The Y coordinate of the context name on the credential (vertical position on the template).
|
|
189
189
|
- context_name_color: The color of the context name on the credential (hexadecimal color code).
|
|
190
|
+
- context_name_size: The font size of the context name on the credential. The default value is 28.
|
|
190
191
|
- issue_date_y: The Y coordinate of the issue date on the credential (vertical position on the template).
|
|
191
192
|
- issue_date_color: The color of the issue date on the credential (hexadecimal color code).
|
|
192
193
|
"""
|
|
@@ -5,7 +5,7 @@ import model_utils.fields
|
|
|
5
5
|
import opaque_keys.edx.django.models
|
|
6
6
|
import uuid
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import learning_credentials.models
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Migration(migrations.Migration):
|
|
@@ -39,7 +39,7 @@ class Migration(migrations.Migration):
|
|
|
39
39
|
models.FileField(
|
|
40
40
|
help_text='Asset file. It could be a PDF template, image or font file.',
|
|
41
41
|
max_length=255,
|
|
42
|
-
upload_to=
|
|
42
|
+
upload_to=learning_credentials.models.CredentialAsset.template_assets_path,
|
|
43
43
|
),
|
|
44
44
|
),
|
|
45
45
|
(
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def migrate_data_if_tables_exist(apps, schema_editor):
|
|
5
|
+
"""Migrate data from openedx_certificates to learning_credentials tables if the source tables exist."""
|
|
6
|
+
connection = schema_editor.connection
|
|
7
|
+
cursor = connection.cursor()
|
|
8
|
+
table_names = connection.introspection.table_names(cursor)
|
|
9
|
+
|
|
10
|
+
tables_to_migrate = [
|
|
11
|
+
(
|
|
12
|
+
"openedx_certificates_externalcertificatetype",
|
|
13
|
+
"learning_credentials_externalcertificatetype",
|
|
14
|
+
"id, created, modified, name, retrieval_func, generation_func, custom_options",
|
|
15
|
+
),
|
|
16
|
+
(
|
|
17
|
+
"openedx_certificates_externalcertificatecourseconfiguration",
|
|
18
|
+
"learning_credentials_externalcertificatecourseconfiguration",
|
|
19
|
+
"id, created, modified, course_id, custom_options, certificate_type_id, periodic_task_id",
|
|
20
|
+
),
|
|
21
|
+
(
|
|
22
|
+
"openedx_certificates_externalcertificateasset",
|
|
23
|
+
"learning_credentials_externalcertificateasset",
|
|
24
|
+
"id, created, modified, description, asset, asset_slug",
|
|
25
|
+
),
|
|
26
|
+
(
|
|
27
|
+
"openedx_certificates_externalcertificate",
|
|
28
|
+
"learning_credentials_externalcertificate",
|
|
29
|
+
"uuid, created, modified, user_id, user_full_name, course_id, certificate_type, status, download_url, legacy_id, generation_task_id",
|
|
30
|
+
),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
for source_table, target_table, fields in tables_to_migrate:
|
|
34
|
+
if source_table in table_names:
|
|
35
|
+
cursor.execute(f"INSERT INTO {target_table} ({fields}) SELECT {fields} FROM {source_table};")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Migration(migrations.Migration):
|
|
39
|
+
dependencies = [("learning_credentials", "0001_initial")]
|
|
40
|
+
operations = [migrations.RunPython(migrate_data_if_tables_exist, migrations.RunPython.noop)]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Clean up legacy `openedx_certificates` tables.
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("learning_credentials", "0005_rename_processors_and_generators"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RunSQL(
|
|
13
|
+
sql=[
|
|
14
|
+
"DROP TABLE IF EXISTS openedx_certificates_externalcertificate;",
|
|
15
|
+
"DROP TABLE IF EXISTS openedx_certificates_externalcertificateasset;",
|
|
16
|
+
"DROP TABLE IF EXISTS openedx_certificates_externalcertificatecourseconfiguration;",
|
|
17
|
+
"DROP TABLE IF EXISTS openedx_certificates_externalcertificatetype;",
|
|
18
|
+
],
|
|
19
|
+
reverse_sql=migrations.RunSQL.noop,
|
|
20
|
+
),
|
|
21
|
+
]
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/models.py
RENAMED
|
@@ -106,7 +106,7 @@ class CredentialConfiguration(TimeStampedModel):
|
|
|
106
106
|
|
|
107
107
|
def save(self, *args, **kwargs):
|
|
108
108
|
"""Create a new PeriodicTask every time a new CredentialConfiguration is created."""
|
|
109
|
-
from learning_credentials.tasks import generate_credentials_for_config_task as task #
|
|
109
|
+
from learning_credentials.tasks import generate_credentials_for_config_task as task # noqa: PLC0415
|
|
110
110
|
|
|
111
111
|
# Use __wrapped__ to get the original function, as the task is wrapped by the @app.task decorator.
|
|
112
112
|
task_path = f"{task.__wrapped__.__module__}.{task.__wrapped__.__name__}"
|
|
@@ -291,7 +291,7 @@ class Credential(TimeStampedModel):
|
|
|
291
291
|
|
|
292
292
|
def send_email(self):
|
|
293
293
|
"""Send a credential link to the student."""
|
|
294
|
-
|
|
294
|
+
learning_context_name = get_learning_context_name(self.learning_context_key)
|
|
295
295
|
user = get_user_model().objects.get(id=self.user_id)
|
|
296
296
|
msg = Message(
|
|
297
297
|
name="certificate_generated",
|
|
@@ -300,7 +300,7 @@ class Credential(TimeStampedModel):
|
|
|
300
300
|
language='en',
|
|
301
301
|
context={
|
|
302
302
|
'certificate_link': self.download_url,
|
|
303
|
-
'course_name':
|
|
303
|
+
'course_name': learning_context_name,
|
|
304
304
|
'platform_name': settings.PLATFORM_NAME,
|
|
305
305
|
},
|
|
306
306
|
)
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/processors.py
RENAMED
|
@@ -45,12 +45,14 @@ def _process_learning_context(
|
|
|
45
45
|
Process a learning context (course or learning path) using the given course processor function.
|
|
46
46
|
|
|
47
47
|
For courses, runs the processor directly. For learning paths, runs the processor on each
|
|
48
|
-
course in the path
|
|
48
|
+
course in the path with step-specific options (if available), and returns the intersection
|
|
49
|
+
of eligible users across all courses.
|
|
49
50
|
|
|
50
51
|
Args:
|
|
51
52
|
learning_context_key: A course key or learning path key to process
|
|
52
53
|
course_processor: A function that processes a single course and returns eligible user IDs
|
|
53
|
-
options: Options to pass to the processor
|
|
54
|
+
options: Options to pass to the processor. For learning paths, may contain a "steps" key
|
|
55
|
+
with step-specific options in the format: {"steps": {"<course_key>": {...}}}
|
|
54
56
|
|
|
55
57
|
Returns:
|
|
56
58
|
A list of eligible user IDs
|
|
@@ -62,12 +64,19 @@ def _process_learning_context(
|
|
|
62
64
|
|
|
63
65
|
results = None
|
|
64
66
|
for course in learning_path.steps.all():
|
|
65
|
-
|
|
67
|
+
course_options = options.get("steps", {}).get(str(course.course_key), options)
|
|
68
|
+
course_results = set(course_processor(course.course_key, course_options))
|
|
69
|
+
|
|
66
70
|
if results is None:
|
|
67
71
|
results = course_results
|
|
68
72
|
else:
|
|
69
73
|
results &= course_results
|
|
70
74
|
|
|
75
|
+
# Filter out users who are not enrolled in the Learning Path.
|
|
76
|
+
results &= set(
|
|
77
|
+
learning_path.enrolled_users.filter(learningpathenrollment__is_active=True).values_list('id', flat=True),
|
|
78
|
+
)
|
|
79
|
+
|
|
71
80
|
return list(results) if results else []
|
|
72
81
|
|
|
73
82
|
|
|
@@ -182,7 +191,7 @@ def retrieve_subsection_grades(learning_context_key: LearningContextKey, options
|
|
|
182
191
|
Options:
|
|
183
192
|
- required_grades: A dictionary of required grades for each category, where the keys are the category names and
|
|
184
193
|
the values are the minimum required grades. The grades are percentages, so they should be in the range [0, 1].
|
|
185
|
-
See the following example
|
|
194
|
+
See the following example:
|
|
186
195
|
|
|
187
196
|
{
|
|
188
197
|
"required_grades": {
|
|
@@ -200,6 +209,28 @@ def retrieve_subsection_grades(learning_context_key: LearningContextKey, options
|
|
|
200
209
|
3. Exam: 70%
|
|
201
210
|
The grades for the Total category will be calculated as follows:
|
|
202
211
|
total_grade = (homework_grade * 0.2) + (lab_grade * 0.1) + (exam_grade * 0.7)
|
|
212
|
+
- steps: For learning paths only. A dictionary with step-specific options in the format
|
|
213
|
+
{"<course_key>": {...}}. If provided, each course in the learning path will use its specific
|
|
214
|
+
options instead of the global options. Example:
|
|
215
|
+
|
|
216
|
+
{
|
|
217
|
+
"required_grades": {
|
|
218
|
+
"Total": 0.8
|
|
219
|
+
},
|
|
220
|
+
"steps": {
|
|
221
|
+
"course-v1:edX+DemoX+Demo_Course": {
|
|
222
|
+
"required_grades": {
|
|
223
|
+
"Total": 0.9
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"course-v1:edX+CS101+2023": {
|
|
227
|
+
"required_grades": {
|
|
228
|
+
"Homework": 0.5,
|
|
229
|
+
"Total": 0.7
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
203
234
|
"""
|
|
204
235
|
return _process_learning_context(learning_context_key, _retrieve_course_subsection_grades, options)
|
|
205
236
|
|
|
@@ -240,6 +271,7 @@ def _retrieve_course_completions(course_id: CourseKey, options: dict[str, Any])
|
|
|
240
271
|
required_completion = options.get('required_completion', 0.9)
|
|
241
272
|
|
|
242
273
|
url = f'/completion-aggregator/v1/course/{course_id}/'
|
|
274
|
+
# The API supports up to 10k results per page, but we limit it to 1k to avoid performance issues.
|
|
243
275
|
query_params = {'page_size': 1000, 'page': 1}
|
|
244
276
|
|
|
245
277
|
# TODO: Extract the logic of this view into an API. The current approach is very hacky.
|
|
@@ -271,6 +303,21 @@ def retrieve_completions(learning_context_key: LearningContextKey, options: dict
|
|
|
271
303
|
|
|
272
304
|
Options:
|
|
273
305
|
- required_completion: The minimum required completion percentage. The default value is 0.9.
|
|
306
|
+
- steps: For learning paths only. A dictionary with step-specific options in the format
|
|
307
|
+
{"<course_key>": {...}}. If provided, each course in the learning path will use its specific
|
|
308
|
+
options instead of the global options. Example:
|
|
309
|
+
|
|
310
|
+
{
|
|
311
|
+
"required_completion": 0.8,
|
|
312
|
+
"steps": {
|
|
313
|
+
"course-v1:edX+DemoX+Demo_Course": {
|
|
314
|
+
"required_completion": 0.9
|
|
315
|
+
},
|
|
316
|
+
"course-v1:edX+CS101+2023": {
|
|
317
|
+
"required_completion": 0.7
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
274
321
|
"""
|
|
275
322
|
return _process_learning_context(learning_context_key, _retrieve_course_completions, options)
|
|
276
323
|
|
|
@@ -289,8 +336,7 @@ def retrieve_completions_and_grades(learning_context_key: LearningContextKey, op
|
|
|
289
336
|
Options:
|
|
290
337
|
- required_completion: The minimum required completion percentage (default: 0.9)
|
|
291
338
|
- required_grades: A dictionary of required grades for each category, where the keys are the category names and
|
|
292
|
-
the values are the minimum required grades. The grades are percentages in the range [0, 1].
|
|
293
|
-
Example::
|
|
339
|
+
the values are the minimum required grades. The grades are percentages in the range [0, 1]. Example:
|
|
294
340
|
|
|
295
341
|
{
|
|
296
342
|
"required_grades": {
|
|
@@ -299,6 +345,31 @@ def retrieve_completions_and_grades(learning_context_key: LearningContextKey, op
|
|
|
299
345
|
"Total": 0.8
|
|
300
346
|
}
|
|
301
347
|
}
|
|
348
|
+
- steps: For learning paths only. A dictionary with step-specific options in the format
|
|
349
|
+
{"<course_key>": {...}}. If provided, each course in the learning path will use its specific
|
|
350
|
+
options instead of the global options. Example:
|
|
351
|
+
|
|
352
|
+
{
|
|
353
|
+
"required_completion": 0.8,
|
|
354
|
+
"required_grades": {
|
|
355
|
+
"Total": 0.7
|
|
356
|
+
},
|
|
357
|
+
"steps": {
|
|
358
|
+
"course-v1:edX+DemoX+Demo_Course": {
|
|
359
|
+
"required_completion": 0.9,
|
|
360
|
+
"required_grades": {
|
|
361
|
+
"Total": 0.8
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"course-v1:edX+CS101+2023": {
|
|
365
|
+
"required_completion": 0.7,
|
|
366
|
+
"required_grades": {
|
|
367
|
+
"Homework": 0.5,
|
|
368
|
+
"Total": 0.6
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
302
373
|
"""
|
|
303
374
|
completion_eligible_users = set(retrieve_completions(learning_context_key, options))
|
|
304
375
|
grades_eligible_users = set(retrieve_subsection_grades(learning_context_key, options))
|
|
@@ -7,6 +7,3 @@ def plugin_settings(settings: Settings):
|
|
|
7
7
|
"""Add `django_celery_beat` to `INSTALLED_APPS`."""
|
|
8
8
|
if 'django_celery_beat' not in settings.INSTALLED_APPS:
|
|
9
9
|
settings.INSTALLED_APPS += ('django_celery_beat',)
|
|
10
|
-
# Temporary app to handle migrations.
|
|
11
|
-
if 'openedx_certificates' not in settings.INSTALLED_APPS:
|
|
12
|
-
settings.INSTALLED_APPS += ('openedx_certificates',)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learning-credentials
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2rc1
|
|
4
4
|
Summary: A pluggable service for preparing Open edX credentials.
|
|
5
5
|
Home-page: https://github.com/open-craft/learning-credentials
|
|
6
6
|
Author: OpenCraft
|
|
@@ -28,7 +28,7 @@ Requires-Dist: django_reverse_admin
|
|
|
28
28
|
Requires-Dist: djangorestframework
|
|
29
29
|
Requires-Dist: edx-opaque-keys
|
|
30
30
|
Requires-Dist: edx_ace
|
|
31
|
-
Requires-Dist: learning-paths-plugin
|
|
31
|
+
Requires-Dist: learning-paths-plugin>=0.3.4
|
|
32
32
|
Requires-Dist: openedx-completion-aggregator
|
|
33
33
|
Requires-Dist: pypdf
|
|
34
34
|
Requires-Dist: reportlab
|
|
@@ -139,7 +139,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
139
139
|
:target: https://pypi.python.org/pypi/learning-credentials/
|
|
140
140
|
:alt: PyPI
|
|
141
141
|
|
|
142
|
-
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/workflows/
|
|
142
|
+
.. |ci-badge| image:: https://github.com/open-craft/learning-credentials/actions/workflows/ci.yml/badge.svg?branch=main
|
|
143
143
|
:target: https://github.com/open-craft/learning-credentials/actions
|
|
144
144
|
:alt: CI
|
|
145
145
|
|
|
@@ -159,7 +159,7 @@ Please do not report security issues in public. Please email security@openedx.or
|
|
|
159
159
|
:target: https://github.com/open-craft/learning-credentials/blob/main/LICENSE.txt
|
|
160
160
|
:alt: License
|
|
161
161
|
|
|
162
|
-
.. |status-badge| image:: https://img.shields.io/badge/Status-
|
|
162
|
+
.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
|
|
163
163
|
:alt: Status
|
|
164
164
|
|
|
165
165
|
.. https://githubnext.com/projects/repo-visualization/
|
|
@@ -186,13 +186,33 @@ Unreleased
|
|
|
186
186
|
|
|
187
187
|
*
|
|
188
188
|
|
|
189
|
-
0.2.
|
|
189
|
+
0.2.2 - 2025-08-05
|
|
190
|
+
|
|
191
|
+
Added
|
|
192
|
+
=====
|
|
193
|
+
|
|
194
|
+
* Step-specific options support for Learning Path credentials.
|
|
195
|
+
|
|
196
|
+
Removed
|
|
197
|
+
=======
|
|
198
|
+
|
|
199
|
+
* Legacy `openedx_certificates` app.
|
|
200
|
+
|
|
201
|
+
0.2.1 – 2025-05-05
|
|
202
|
+
******************
|
|
203
|
+
|
|
204
|
+
Fixed
|
|
205
|
+
=====
|
|
206
|
+
|
|
207
|
+
* Check enrollment status before issuing Learning Path credentials.
|
|
208
|
+
|
|
209
|
+
0.2.0 – 2025-04-03
|
|
190
210
|
******************
|
|
191
211
|
|
|
192
212
|
Added
|
|
193
213
|
=====
|
|
194
214
|
|
|
195
|
-
*
|
|
215
|
+
* Learning Paths support.
|
|
196
216
|
|
|
197
217
|
|
|
198
218
|
0.1.0 – 2025-01-29
|
|
@@ -27,6 +27,7 @@ learning_credentials/migrations/0002_migrate_to_learning_credentials.py
|
|
|
27
27
|
learning_credentials/migrations/0003_rename_certificates_to_credentials.py
|
|
28
28
|
learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py
|
|
29
29
|
learning_credentials/migrations/0005_rename_processors_and_generators.py
|
|
30
|
+
learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py
|
|
30
31
|
learning_credentials/migrations/__init__.py
|
|
31
32
|
learning_credentials/settings/__init__.py
|
|
32
33
|
learning_credentials/settings/common.py
|
|
@@ -37,11 +38,6 @@ learning_credentials/templates/learning_credentials/edx_ace/certificate_generate
|
|
|
37
38
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt
|
|
38
39
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html
|
|
39
40
|
learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt
|
|
40
|
-
openedx_certificates/__init__.py
|
|
41
|
-
openedx_certificates/apps.py
|
|
42
|
-
openedx_certificates/models.py
|
|
43
|
-
openedx_certificates/migrations/0001_initial.py
|
|
44
|
-
openedx_certificates/migrations/__init__.py
|
|
45
41
|
requirements/base.in
|
|
46
42
|
requirements/constraints.txt
|
|
47
43
|
tests/test_generators.py
|
|
@@ -14,5 +14,4 @@ pypdf # PDF manipulation library
|
|
|
14
14
|
reportlab # PDF generation library
|
|
15
15
|
openedx-completion-aggregator # Completion aggregation service
|
|
16
16
|
edx_ace # Messaging library
|
|
17
|
-
|
|
18
|
-
learning-paths-plugin==0.3.0rc1
|
|
17
|
+
learning-paths-plugin>=0.3.4
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/requirements/constraints.txt
RENAMED
|
@@ -9,4 +9,7 @@
|
|
|
9
9
|
# linking to it here is good.
|
|
10
10
|
|
|
11
11
|
# Common constraints for edx repos
|
|
12
|
-
-c common_constraints.txt
|
|
12
|
+
-c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
|
|
13
|
+
|
|
14
|
+
# TODO: Delete this once https://github.com/googleapis/google-auth-library-python/pull/1797 is merged.
|
|
15
|
+
cachetools<6
|
|
@@ -143,13 +143,12 @@ setup(
|
|
|
143
143
|
author_email='help@opencraft.com',
|
|
144
144
|
url='https://github.com/open-craft/learning-credentials',
|
|
145
145
|
packages=find_packages(
|
|
146
|
-
include=['learning_credentials', 'learning_credentials.*'
|
|
146
|
+
include=['learning_credentials', 'learning_credentials.*'],
|
|
147
147
|
exclude=["*tests"],
|
|
148
148
|
),
|
|
149
149
|
entry_points={
|
|
150
150
|
"lms.djangoapp": [
|
|
151
151
|
"learning_credentials = learning_credentials.apps:LearningCredentialsConfig",
|
|
152
|
-
"openedx_certificates = openedx_certificates.apps:OpenEdxCertificatesConfig",
|
|
153
152
|
],
|
|
154
153
|
},
|
|
155
154
|
include_package_data=True,
|
|
@@ -75,6 +75,7 @@ def test_register_font_with_custom_font(mock_register_font: Mock, mock_font_clas
|
|
|
75
75
|
'name_color': '123',
|
|
76
76
|
'context_name_color': '#9B192A',
|
|
77
77
|
'issue_date_color': '#f59a8e',
|
|
78
|
+
'context_name_size': 20,
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
81
|
'name_color': (17 / 255, 34 / 255, 51 / 255),
|
|
@@ -135,7 +136,7 @@ def test_write_text_on_template(mock_canvas_class: Mock, context_name: str, opti
|
|
|
135
136
|
assert canvas_object.drawString.call_args_list[0] == call(expected_name_x, expected_name_y, username)
|
|
136
137
|
assert mock_canvas_class.return_value.stringWidth.mock_calls[0][1] == (username,)
|
|
137
138
|
|
|
138
|
-
assert canvas_object.setFont.call_args_list[1] == call(font, 28)
|
|
139
|
+
assert canvas_object.setFont.call_args_list[1] == call(font, options.get('context_name_size', 28))
|
|
139
140
|
assert canvas_object.setFillColorRGB.call_args_list[1] == call(*expected_context_name_color)
|
|
140
141
|
|
|
141
142
|
assert canvas_object.setFont.call_args_list[2] == call(font, 12)
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
from unittest.mock import Mock, call, patch
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
8
9
|
from django.http import QueryDict
|
|
9
|
-
from learning_paths.models import LearningPath
|
|
10
|
+
from learning_paths.models import LearningPath
|
|
10
11
|
from opaque_keys.edx.keys import CourseKey
|
|
11
12
|
|
|
12
13
|
# noinspection PyProtectedMember
|
|
@@ -19,6 +20,10 @@ from learning_credentials.processors import (
|
|
|
19
20
|
retrieve_completions_and_grades,
|
|
20
21
|
retrieve_subsection_grades,
|
|
21
22
|
)
|
|
23
|
+
from test_utils.factories import UserFactory
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from django.contrib.auth.models import User
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
@patch(
|
|
@@ -322,66 +327,85 @@ def test_retrieve_course_completions_and_grades(
|
|
|
322
327
|
|
|
323
328
|
|
|
324
329
|
@pytest.fixture
|
|
325
|
-
def
|
|
330
|
+
def users() -> list[User]:
|
|
331
|
+
"""Create a list of users."""
|
|
332
|
+
return UserFactory.create_batch(6)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@pytest.fixture
|
|
336
|
+
def learning_path_with_courses(users: list[User]) -> LearningPath:
|
|
326
337
|
"""Create a LearningPath with multiple course steps."""
|
|
327
|
-
learning_path = LearningPath.objects.create(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
learning_path = LearningPath.objects.create(key='path-v1:test+number+run+group')
|
|
339
|
+
|
|
340
|
+
for i in range(3):
|
|
341
|
+
learning_path.steps.create(course_key=f"course-v1:TestX+Test101+2023_{i}", order=i)
|
|
331
342
|
|
|
332
|
-
|
|
343
|
+
# Enroll all users except the last one.
|
|
344
|
+
for i in range(len(users) - 1):
|
|
345
|
+
learning_path.enrolled_users.add(users[i])
|
|
333
346
|
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
# Mark the second last user's enrollment as inactive.
|
|
348
|
+
learning_path.learningpathenrollment_set.filter(user=users[-2]).update(is_active=False)
|
|
336
349
|
|
|
337
350
|
return learning_path
|
|
338
351
|
|
|
339
352
|
|
|
353
|
+
@pytest.mark.parametrize(
|
|
354
|
+
('patch_target', 'function_to_test'),
|
|
355
|
+
[
|
|
356
|
+
("learning_credentials.processors._retrieve_course_subsection_grades", retrieve_subsection_grades),
|
|
357
|
+
("learning_credentials.processors._retrieve_course_completions", retrieve_completions),
|
|
358
|
+
],
|
|
359
|
+
ids=['subsection_grades', 'completions'],
|
|
360
|
+
)
|
|
340
361
|
@pytest.mark.django_db
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
362
|
+
def test_retrieve_data_for_learning_path(
|
|
363
|
+
patch_target: str,
|
|
364
|
+
function_to_test: callable,
|
|
344
365
|
learning_path_with_courses: LearningPath,
|
|
366
|
+
users: list[User],
|
|
345
367
|
):
|
|
346
|
-
"""Test retrieving
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
368
|
+
"""Test retrieving data for a learning path."""
|
|
369
|
+
with patch(patch_target) as mock_retrieve:
|
|
370
|
+
options = {}
|
|
371
|
+
mock_retrieve.side_effect = (
|
|
372
|
+
(users[i].id for i in (0, 1, 2, 4, 5)), # Users passing/completing course0
|
|
373
|
+
(users[i].id for i in (0, 1, 2, 3, 4, 5)), # Users passing/completing course1
|
|
374
|
+
(users[i].id for i in (0, 2, 3, 4, 5)), # Users passing/completing course2
|
|
375
|
+
)
|
|
353
376
|
|
|
354
|
-
|
|
377
|
+
result = function_to_test(learning_path_with_courses.key, options)
|
|
355
378
|
|
|
356
|
-
|
|
379
|
+
assert sorted(result) == [users[0].id, users[2].id]
|
|
357
380
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
381
|
+
assert mock_retrieve.call_count == 3
|
|
382
|
+
course_keys = [step.course_key for step in learning_path_with_courses.steps.all()]
|
|
383
|
+
for i, course_key in enumerate(course_keys):
|
|
384
|
+
call_args = mock_retrieve.call_args_list[i]
|
|
385
|
+
assert call_args[0] == (course_key, options)
|
|
363
386
|
|
|
364
387
|
|
|
365
|
-
@pytest.mark.django_db
|
|
366
388
|
@patch("learning_credentials.processors._retrieve_course_completions")
|
|
367
|
-
|
|
368
|
-
|
|
389
|
+
@pytest.mark.django_db
|
|
390
|
+
def test_retrieve_data_for_learning_path_with_step_options(
|
|
391
|
+
mock_retrieve: Mock,
|
|
369
392
|
learning_path_with_courses: LearningPath,
|
|
370
393
|
):
|
|
371
|
-
"""Test retrieving
|
|
372
|
-
|
|
373
|
-
mock_retrieve_course_completions.side_effect = [
|
|
374
|
-
[101, 102, 103, 106], # Users completing course0
|
|
375
|
-
[102, 103, 104, 106], # Users completing course1
|
|
376
|
-
[103, 105, 106], # Users completing course2
|
|
377
|
-
]
|
|
394
|
+
"""Test retrieving data for a learning path with step-specific options."""
|
|
395
|
+
course_keys = [step.course_key for step in learning_path_with_courses.steps.all()]
|
|
378
396
|
|
|
379
|
-
|
|
397
|
+
options = {
|
|
398
|
+
"required_completion": 0.7,
|
|
399
|
+
"steps": {
|
|
400
|
+
str(course_keys[0]): {"required_completion": 0.8},
|
|
401
|
+
str(course_keys[1]): {"required_completion": 0.9},
|
|
402
|
+
# course_keys[2] will use base options
|
|
403
|
+
},
|
|
404
|
+
}
|
|
380
405
|
|
|
381
|
-
|
|
406
|
+
retrieve_completions(learning_path_with_courses.key, options)
|
|
382
407
|
|
|
383
|
-
assert
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
assert call_args[0] == (course_key, options)
|
|
408
|
+
assert mock_retrieve.call_count == 3
|
|
409
|
+
assert mock_retrieve.call_args_list[0][0] == (course_keys[0], options["steps"][str(course_keys[0])])
|
|
410
|
+
assert mock_retrieve.call_args_list[1][0] == (course_keys[1], options["steps"][str(course_keys[1])])
|
|
411
|
+
assert mock_retrieve.call_args_list[2][0] == (course_keys[2], options)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from django.db import migrations
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Migration(migrations.Migration):
|
|
5
|
-
dependencies = [
|
|
6
|
-
('openedx_certificates', '0001_initial'),
|
|
7
|
-
('learning_credentials', '0001_initial'),
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
operations = [
|
|
11
|
-
migrations.RunSQL(
|
|
12
|
-
sql=[
|
|
13
|
-
"INSERT INTO learning_credentials_externalcertificatetype (id, created, modified, name, retrieval_func, generation_func, custom_options) "
|
|
14
|
-
"SELECT id, created, modified, name, retrieval_func, generation_func, custom_options FROM openedx_certificates_externalcertificatetype;",
|
|
15
|
-
|
|
16
|
-
"INSERT INTO learning_credentials_externalcertificatecourseconfiguration (id, created, modified, course_id, custom_options, certificate_type_id, periodic_task_id) "
|
|
17
|
-
"SELECT id, created, modified, course_id, custom_options, certificate_type_id, periodic_task_id FROM openedx_certificates_externalcertificatecourseconfiguration;",
|
|
18
|
-
|
|
19
|
-
"INSERT INTO learning_credentials_externalcertificateasset (id, created, modified, description, asset, asset_slug) "
|
|
20
|
-
"SELECT id, created, modified, description, asset, asset_slug FROM openedx_certificates_externalcertificateasset;",
|
|
21
|
-
|
|
22
|
-
"INSERT INTO learning_credentials_externalcertificate (uuid, created, modified, user_id, user_full_name, course_id, certificate_type, status, download_url, legacy_id, generation_task_id) "
|
|
23
|
-
"SELECT uuid, created, modified, user_id, user_full_name, course_id, certificate_type, status, download_url, legacy_id, generation_task_id FROM openedx_certificates_externalcertificate;",
|
|
24
|
-
],
|
|
25
|
-
reverse_sql=[
|
|
26
|
-
"DELETE FROM learning_credentials_externalcertificate;",
|
|
27
|
-
"DELETE FROM learning_credentials_externalcertificatecourseconfiguration;",
|
|
28
|
-
"DELETE FROM learning_credentials_externalcertificateasset;",
|
|
29
|
-
"DELETE FROM learning_credentials_externalcertificatetype;",
|
|
30
|
-
],
|
|
31
|
-
),
|
|
32
|
-
]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Legacy module for Open edX certificates. It exists for backward compatibility."""
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""openedx_certificates Django application initialization."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from django.apps import AppConfig
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class OpenedxCertificatesConfig(AppConfig):
|
|
9
|
-
"""Configuration for the openedx_certificates Django application."""
|
|
10
|
-
|
|
11
|
-
name = 'openedx_certificates'
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
# Generated by Django 3.2.23 on 2023-11-14 15:54
|
|
2
|
-
|
|
3
|
-
from django.db import migrations, models
|
|
4
|
-
import django.db.models.deletion
|
|
5
|
-
import django.utils.timezone
|
|
6
|
-
import jsonfield.fields
|
|
7
|
-
import model_utils.fields
|
|
8
|
-
import opaque_keys.edx.django.models
|
|
9
|
-
import openedx_certificates.models
|
|
10
|
-
import uuid
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class Migration(migrations.Migration):
|
|
14
|
-
initial = True
|
|
15
|
-
|
|
16
|
-
dependencies = [
|
|
17
|
-
('django_celery_beat', '0018_improve_crontab_helptext'),
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
operations = [
|
|
21
|
-
migrations.CreateModel(
|
|
22
|
-
name='ExternalCertificateAsset',
|
|
23
|
-
fields=[
|
|
24
|
-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
25
|
-
(
|
|
26
|
-
'created',
|
|
27
|
-
model_utils.fields.AutoCreatedField(
|
|
28
|
-
default=django.utils.timezone.now, editable=False, verbose_name='created'
|
|
29
|
-
),
|
|
30
|
-
),
|
|
31
|
-
(
|
|
32
|
-
'modified',
|
|
33
|
-
model_utils.fields.AutoLastModifiedField(
|
|
34
|
-
default=django.utils.timezone.now, editable=False, verbose_name='modified'
|
|
35
|
-
),
|
|
36
|
-
),
|
|
37
|
-
('description', models.CharField(blank=True, help_text='Description of the asset.', max_length=255)),
|
|
38
|
-
(
|
|
39
|
-
'asset',
|
|
40
|
-
models.FileField(
|
|
41
|
-
help_text='Asset file. It could be a PDF template, image or font file.',
|
|
42
|
-
max_length=255,
|
|
43
|
-
upload_to=openedx_certificates.models.ExternalCertificateAsset.template_assets_path,
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
(
|
|
47
|
-
'asset_slug',
|
|
48
|
-
models.SlugField(
|
|
49
|
-
help_text="Asset's unique slug. We can reference the asset in templates using this value.",
|
|
50
|
-
max_length=255,
|
|
51
|
-
unique=True,
|
|
52
|
-
),
|
|
53
|
-
),
|
|
54
|
-
],
|
|
55
|
-
options={
|
|
56
|
-
'get_latest_by': 'created',
|
|
57
|
-
},
|
|
58
|
-
),
|
|
59
|
-
migrations.CreateModel(
|
|
60
|
-
name='ExternalCertificateType',
|
|
61
|
-
fields=[
|
|
62
|
-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
63
|
-
(
|
|
64
|
-
'created',
|
|
65
|
-
model_utils.fields.AutoCreatedField(
|
|
66
|
-
default=django.utils.timezone.now, editable=False, verbose_name='created'
|
|
67
|
-
),
|
|
68
|
-
),
|
|
69
|
-
(
|
|
70
|
-
'modified',
|
|
71
|
-
model_utils.fields.AutoLastModifiedField(
|
|
72
|
-
default=django.utils.timezone.now, editable=False, verbose_name='modified'
|
|
73
|
-
),
|
|
74
|
-
),
|
|
75
|
-
('name', models.CharField(help_text='Name of the certificate type.', max_length=255, unique=True)),
|
|
76
|
-
(
|
|
77
|
-
'retrieval_func',
|
|
78
|
-
models.CharField(help_text='A name of the function to retrieve eligible users.', max_length=200),
|
|
79
|
-
),
|
|
80
|
-
(
|
|
81
|
-
'generation_func',
|
|
82
|
-
models.CharField(help_text='A name of the function to generate certificates.', max_length=200),
|
|
83
|
-
),
|
|
84
|
-
(
|
|
85
|
-
'custom_options',
|
|
86
|
-
jsonfield.fields.JSONField(blank=True, default=dict, help_text='Custom options for the functions.'),
|
|
87
|
-
),
|
|
88
|
-
],
|
|
89
|
-
options={
|
|
90
|
-
'abstract': False,
|
|
91
|
-
},
|
|
92
|
-
),
|
|
93
|
-
migrations.CreateModel(
|
|
94
|
-
name='ExternalCertificate',
|
|
95
|
-
fields=[
|
|
96
|
-
(
|
|
97
|
-
'created',
|
|
98
|
-
model_utils.fields.AutoCreatedField(
|
|
99
|
-
default=django.utils.timezone.now, editable=False, verbose_name='created'
|
|
100
|
-
),
|
|
101
|
-
),
|
|
102
|
-
(
|
|
103
|
-
'modified',
|
|
104
|
-
model_utils.fields.AutoLastModifiedField(
|
|
105
|
-
default=django.utils.timezone.now, editable=False, verbose_name='modified'
|
|
106
|
-
),
|
|
107
|
-
),
|
|
108
|
-
(
|
|
109
|
-
'uuid',
|
|
110
|
-
models.UUIDField(
|
|
111
|
-
default=uuid.uuid4,
|
|
112
|
-
editable=False,
|
|
113
|
-
help_text='Auto-generated UUID of the certificate',
|
|
114
|
-
primary_key=True,
|
|
115
|
-
serialize=False,
|
|
116
|
-
),
|
|
117
|
-
),
|
|
118
|
-
('user_id', models.IntegerField(help_text='ID of the user receiving the certificate')),
|
|
119
|
-
('user_full_name', models.CharField(help_text='User receiving the certificate', max_length=255)),
|
|
120
|
-
(
|
|
121
|
-
'course_id',
|
|
122
|
-
opaque_keys.edx.django.models.CourseKeyField(
|
|
123
|
-
help_text='ID of a course for which the certificate was issued', max_length=255
|
|
124
|
-
),
|
|
125
|
-
),
|
|
126
|
-
('certificate_type', models.CharField(help_text='Type of the certificate', max_length=255)),
|
|
127
|
-
(
|
|
128
|
-
'status',
|
|
129
|
-
models.CharField(
|
|
130
|
-
choices=[
|
|
131
|
-
('generating', 'Generating'),
|
|
132
|
-
('available', 'Available'),
|
|
133
|
-
('error', 'Error'),
|
|
134
|
-
('invalidated', 'Invalidated'),
|
|
135
|
-
],
|
|
136
|
-
default='generating',
|
|
137
|
-
help_text='Status of the certificate generation task',
|
|
138
|
-
max_length=32,
|
|
139
|
-
),
|
|
140
|
-
),
|
|
141
|
-
(
|
|
142
|
-
'download_url',
|
|
143
|
-
models.URLField(blank=True, help_text='URL of the generated certificate PDF (e.g., to S3)'),
|
|
144
|
-
),
|
|
145
|
-
(
|
|
146
|
-
'legacy_id',
|
|
147
|
-
models.IntegerField(
|
|
148
|
-
help_text='Legacy ID of the certificate imported from another system', null=True
|
|
149
|
-
),
|
|
150
|
-
),
|
|
151
|
-
('generation_task_id', models.CharField(help_text='Task ID from the Celery queue', max_length=255)),
|
|
152
|
-
],
|
|
153
|
-
options={
|
|
154
|
-
'unique_together': {('user_id', 'course_id', 'certificate_type')},
|
|
155
|
-
},
|
|
156
|
-
),
|
|
157
|
-
migrations.CreateModel(
|
|
158
|
-
name='ExternalCertificateCourseConfiguration',
|
|
159
|
-
fields=[
|
|
160
|
-
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
161
|
-
(
|
|
162
|
-
'created',
|
|
163
|
-
model_utils.fields.AutoCreatedField(
|
|
164
|
-
default=django.utils.timezone.now, editable=False, verbose_name='created'
|
|
165
|
-
),
|
|
166
|
-
),
|
|
167
|
-
(
|
|
168
|
-
'modified',
|
|
169
|
-
model_utils.fields.AutoLastModifiedField(
|
|
170
|
-
default=django.utils.timezone.now, editable=False, verbose_name='modified'
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
(
|
|
174
|
-
'course_id',
|
|
175
|
-
opaque_keys.edx.django.models.CourseKeyField(help_text='The ID of the course.', max_length=255),
|
|
176
|
-
),
|
|
177
|
-
(
|
|
178
|
-
'custom_options',
|
|
179
|
-
jsonfield.fields.JSONField(
|
|
180
|
-
blank=True,
|
|
181
|
-
default=dict,
|
|
182
|
-
help_text='Custom options for the functions. If specified, they are merged with the options defined in the certificate type.',
|
|
183
|
-
),
|
|
184
|
-
),
|
|
185
|
-
(
|
|
186
|
-
'certificate_type',
|
|
187
|
-
models.ForeignKey(
|
|
188
|
-
help_text='Associated certificate type.',
|
|
189
|
-
on_delete=django.db.models.deletion.CASCADE,
|
|
190
|
-
to='openedx_certificates.externalcertificatetype',
|
|
191
|
-
),
|
|
192
|
-
),
|
|
193
|
-
(
|
|
194
|
-
'periodic_task',
|
|
195
|
-
models.OneToOneField(
|
|
196
|
-
help_text='Associated periodic task.',
|
|
197
|
-
on_delete=django.db.models.deletion.CASCADE,
|
|
198
|
-
to='django_celery_beat.periodictask',
|
|
199
|
-
),
|
|
200
|
-
),
|
|
201
|
-
],
|
|
202
|
-
options={
|
|
203
|
-
'unique_together': {('course_id', 'certificate_type')},
|
|
204
|
-
},
|
|
205
|
-
),
|
|
206
|
-
]
|
|
File without changes
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Proxy models for backward compatibility with the old app structure.
|
|
3
|
-
|
|
4
|
-
These will redirect to the new models in learning_credentials.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from learning_credentials.models import Credential as LearningCredential
|
|
8
|
-
from learning_credentials.models import CredentialAsset as LearningCredentialAsset
|
|
9
|
-
from learning_credentials.models import CredentialConfiguration as LearningCredentialConfiguration
|
|
10
|
-
from learning_credentials.models import CredentialType as LearningCredentialType
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ExternalCertificate(LearningCredential):
|
|
14
|
-
"""Proxy model for backward compatibility."""
|
|
15
|
-
|
|
16
|
-
class Meta: # noqa: D106
|
|
17
|
-
proxy = True
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ExternalCertificateAsset(LearningCredentialAsset):
|
|
21
|
-
"""Proxy model for backward compatibility."""
|
|
22
|
-
|
|
23
|
-
class Meta: # noqa: D106
|
|
24
|
-
proxy = True
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ExternalCertificateType(LearningCredentialType):
|
|
28
|
-
"""Proxy model for backward compatibility."""
|
|
29
|
-
|
|
30
|
-
class Meta: # noqa: D106
|
|
31
|
-
proxy = True
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class ExternalCertificateCourseConfiguration(LearningCredentialConfiguration):
|
|
35
|
-
"""Proxy model for backward compatibility."""
|
|
36
|
-
|
|
37
|
-
class Meta: # noqa: D106
|
|
38
|
-
proxy = True
|
|
File without changes
|
|
File without changes
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/admin.py
RENAMED
|
File without changes
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/apps.py
RENAMED
|
File without changes
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/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
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/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
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/urls.py
RENAMED
|
File without changes
|
{learning_credentials-0.2.0rc3 → learning_credentials-0.2.2rc1}/learning_credentials/views.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|