learning-credentials 0.2.1__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.
Files changed (54) hide show
  1. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/CHANGELOG.rst +12 -0
  2. {learning_credentials-0.2.1/learning_credentials.egg-info → learning_credentials-0.2.2rc1}/PKG-INFO +14 -2
  3. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/__init__.py +1 -1
  4. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/compat.py +2 -0
  5. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0001_initial.py +2 -2
  6. learning_credentials-0.2.2rc1/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +40 -0
  7. learning_credentials-0.2.2rc1/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +21 -0
  8. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/models.py +1 -1
  9. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/processors.py +71 -6
  10. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/settings/common.py +0 -3
  11. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1/learning_credentials.egg-info}/PKG-INFO +14 -2
  12. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/SOURCES.txt +1 -5
  13. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/entry_points.txt +0 -1
  14. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/requires.txt +1 -1
  15. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/top_level.txt +0 -1
  16. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/pyproject.toml +3 -0
  17. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/requirements/base.in +1 -1
  18. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/requirements/constraints.txt +4 -1
  19. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/setup.py +1 -2
  20. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/tests/test_processors.py +27 -1
  21. learning_credentials-0.2.1/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -32
  22. learning_credentials-0.2.1/openedx_certificates/__init__.py +0 -1
  23. learning_credentials-0.2.1/openedx_certificates/apps.py +0 -11
  24. learning_credentials-0.2.1/openedx_certificates/migrations/0001_initial.py +0 -206
  25. learning_credentials-0.2.1/openedx_certificates/migrations/__init__.py +0 -0
  26. learning_credentials-0.2.1/openedx_certificates/models.py +0 -38
  27. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/LICENSE.txt +0 -0
  28. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/MANIFEST.in +0 -0
  29. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/README.rst +0 -0
  30. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/admin.py +0 -0
  31. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/apps.py +0 -0
  32. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/exceptions.py +0 -0
  33. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/generators.py +0 -0
  34. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
  35. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
  36. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
  37. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/migrations/__init__.py +0 -0
  38. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/settings/__init__.py +0 -0
  39. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/settings/production.py +0 -0
  40. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/tasks.py +0 -0
  41. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/base.html +0 -0
  42. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
  43. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
  44. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
  45. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
  46. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
  47. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/urls.py +0 -0
  48. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials/views.py +0 -0
  49. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/dependency_links.txt +0 -0
  50. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/learning_credentials.egg-info/not-zip-safe +0 -0
  51. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/setup.cfg +0 -0
  52. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/tests/test_generators.py +0 -0
  53. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/tests/test_models.py +0 -0
  54. {learning_credentials-0.2.1 → learning_credentials-0.2.2rc1}/tests/test_tasks.py +0 -0
@@ -16,6 +16,18 @@ Unreleased
16
16
 
17
17
  *
18
18
 
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
+
19
31
  0.2.1 – 2025-05-05
20
32
  ******************
21
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.2.1
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>=0.3.0
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
@@ -186,6 +186,18 @@ Unreleased
186
186
 
187
187
  *
188
188
 
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
+
189
201
  0.2.1 – 2025-05-05
190
202
  ******************
191
203
 
@@ -1,3 +1,3 @@
1
1
  """A pluggable service for preparing Open edX credentials."""
2
2
 
3
- __version__ = '0.2.1'
3
+ __version__ = '0.2.2-rc1'
@@ -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
@@ -5,7 +5,7 @@ import model_utils.fields
5
5
  import opaque_keys.edx.django.models
6
6
  import uuid
7
7
 
8
- import openedx_certificates.models
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=openedx_certificates.models.ExternalCertificateAsset.template_assets_path,
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
+ ]
@@ -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 # Avoid circular imports.
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__}"
@@ -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 and returns the intersection of eligible users across all courses.
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,7 +64,9 @@ def _process_learning_context(
62
64
 
63
65
  results = None
64
66
  for course in learning_path.steps.all():
65
- course_results = set(course_processor(course.course_key, options))
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:
@@ -187,7 +191,7 @@ def retrieve_subsection_grades(learning_context_key: LearningContextKey, options
187
191
  Options:
188
192
  - required_grades: A dictionary of required grades for each category, where the keys are the category names and
189
193
  the values are the minimum required grades. The grades are percentages, so they should be in the range [0, 1].
190
- See the following example::
194
+ See the following example:
191
195
 
192
196
  {
193
197
  "required_grades": {
@@ -205,6 +209,28 @@ def retrieve_subsection_grades(learning_context_key: LearningContextKey, options
205
209
  3. Exam: 70%
206
210
  The grades for the Total category will be calculated as follows:
207
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
+ {"&lt;course_key&gt;": {...}}. 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
+ }
208
234
  """
209
235
  return _process_learning_context(learning_context_key, _retrieve_course_subsection_grades, options)
210
236
 
@@ -277,6 +303,21 @@ def retrieve_completions(learning_context_key: LearningContextKey, options: dict
277
303
 
278
304
  Options:
279
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
+ {"&lt;course_key&gt;": {...}}. 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
+ }
280
321
  """
281
322
  return _process_learning_context(learning_context_key, _retrieve_course_completions, options)
282
323
 
@@ -295,8 +336,7 @@ def retrieve_completions_and_grades(learning_context_key: LearningContextKey, op
295
336
  Options:
296
337
  - required_completion: The minimum required completion percentage (default: 0.9)
297
338
  - required_grades: A dictionary of required grades for each category, where the keys are the category names and
298
- the values are the minimum required grades. The grades are percentages in the range [0, 1].
299
- Example::
339
+ the values are the minimum required grades. The grades are percentages in the range [0, 1]. Example:
300
340
 
301
341
  {
302
342
  "required_grades": {
@@ -305,6 +345,31 @@ def retrieve_completions_and_grades(learning_context_key: LearningContextKey, op
305
345
  "Total": 0.8
306
346
  }
307
347
  }
348
+ - steps: For learning paths only. A dictionary with step-specific options in the format
349
+ {"&lt;course_key&gt;": {...}}. 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
+ }
308
373
  """
309
374
  completion_eligible_users = set(retrieve_completions(learning_context_key, options))
310
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.1
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>=0.3.0
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
@@ -186,6 +186,18 @@ Unreleased
186
186
 
187
187
  *
188
188
 
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
+
189
201
  0.2.1 – 2025-05-05
190
202
  ******************
191
203
 
@@ -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
@@ -1,3 +1,2 @@
1
1
  [lms.djangoapp]
2
2
  learning_credentials = learning_credentials.apps:LearningCredentialsConfig
3
- openedx_certificates = openedx_certificates.apps:OpenEdxCertificatesConfig
@@ -7,7 +7,7 @@ django_reverse_admin
7
7
  djangorestframework
8
8
  edx-opaque-keys
9
9
  edx_ace
10
- learning-paths-plugin>=0.3.0
10
+ learning-paths-plugin>=0.3.4
11
11
  openedx-completion-aggregator
12
12
  pypdf
13
13
  reportlab
@@ -1,2 +1 @@
1
1
  learning_credentials
2
- openedx_certificates
@@ -98,6 +98,9 @@ convention = "google"
98
98
  [tool.ruff.lint.pylint]
99
99
  allow-magic-value-types = ['int', 'str']
100
100
 
101
+ [tool.ruff.format]
102
+ quote-style = "preserve"
103
+
101
104
  [tool.black]
102
105
  line-length = 120
103
106
  target-version = ['py311']
@@ -14,4 +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
- learning-paths-plugin>=0.3.0
17
+ learning-paths-plugin>=0.3.4
@@ -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.*', 'openedx_certificates', 'openedx_certificates.*'],
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,
@@ -367,7 +367,7 @@ def test_retrieve_data_for_learning_path(
367
367
  ):
368
368
  """Test retrieving data for a learning path."""
369
369
  with patch(patch_target) as mock_retrieve:
370
- options = Mock()
370
+ options = {}
371
371
  mock_retrieve.side_effect = (
372
372
  (users[i].id for i in (0, 1, 2, 4, 5)), # Users passing/completing course0
373
373
  (users[i].id for i in (0, 1, 2, 3, 4, 5)), # Users passing/completing course1
@@ -383,3 +383,29 @@ def test_retrieve_data_for_learning_path(
383
383
  for i, course_key in enumerate(course_keys):
384
384
  call_args = mock_retrieve.call_args_list[i]
385
385
  assert call_args[0] == (course_key, options)
386
+
387
+
388
+ @patch("learning_credentials.processors._retrieve_course_completions")
389
+ @pytest.mark.django_db
390
+ def test_retrieve_data_for_learning_path_with_step_options(
391
+ mock_retrieve: Mock,
392
+ learning_path_with_courses: LearningPath,
393
+ ):
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()]
396
+
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
+ }
405
+
406
+ retrieve_completions(learning_path_with_courses.key, options)
407
+
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
- ]
@@ -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