learning-credentials 0.4.1rc5__tar.gz → 0.4.1rc6__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 (53) hide show
  1. {learning_credentials-0.4.1rc5/learning_credentials.egg-info → learning_credentials-0.4.1rc6}/PKG-INFO +1 -1
  2. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/models.py +21 -2
  3. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6/learning_credentials.egg-info}/PKG-INFO +1 -1
  4. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/pyproject.toml +1 -1
  5. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_models.py +55 -0
  6. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/CHANGELOG.rst +0 -0
  7. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/LICENSE.txt +0 -0
  8. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/MANIFEST.in +0 -0
  9. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/README.rst +0 -0
  10. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/__init__.py +0 -0
  11. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/admin.py +0 -0
  12. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/__init__.py +0 -0
  13. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/urls.py +0 -0
  14. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/v1/__init__.py +0 -0
  15. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/v1/permissions.py +0 -0
  16. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/v1/urls.py +0 -0
  17. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/api/v1/views.py +0 -0
  18. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/apps.py +0 -0
  19. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/compat.py +0 -0
  20. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/conf/locale/config.yaml +0 -0
  21. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/exceptions.py +0 -0
  22. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/generators.py +0 -0
  23. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0001_initial.py +0 -0
  24. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0002_migrate_to_learning_credentials.py +0 -0
  25. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0003_rename_certificates_to_credentials.py +0 -0
  26. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0004_replace_course_keys_with_learning_context_keys.py +0 -0
  27. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0005_rename_processors_and_generators.py +0 -0
  28. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0006_cleanup_openedx_certificates_tables.py +0 -0
  29. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/0007_migrate_to_text_elements_format.py +0 -0
  30. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/migrations/__init__.py +0 -0
  31. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/processors.py +0 -0
  32. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/settings/__init__.py +0 -0
  33. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/settings/common.py +0 -0
  34. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/settings/production.py +0 -0
  35. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/tasks.py +0 -0
  36. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/base.html +0 -0
  37. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.html +0 -0
  38. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/body.txt +0 -0
  39. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/from_name.txt +0 -0
  40. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/head.html +0 -0
  41. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/templates/learning_credentials/edx_ace/certificate_generated/email/subject.txt +0 -0
  42. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials/urls.py +0 -0
  43. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials.egg-info/SOURCES.txt +0 -0
  44. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials.egg-info/dependency_links.txt +0 -0
  45. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials.egg-info/entry_points.txt +0 -0
  46. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials.egg-info/requires.txt +0 -0
  47. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/learning_credentials.egg-info/top_level.txt +0 -0
  48. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/setup.cfg +0 -0
  49. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_generators.py +0 -0
  50. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_migrations.py +0 -0
  51. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_processors.py +0 -0
  52. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_tasks.py +0 -0
  53. {learning_credentials-0.4.1rc5 → learning_credentials-0.4.1rc6}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.4.1rc5
3
+ Version: 0.4.1rc6
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
@@ -33,6 +33,25 @@ if TYPE_CHECKING: # pragma: no cover
33
33
  log = logging.getLogger(__name__)
34
34
 
35
35
 
36
+ def _deep_merge(base: dict, override: dict) -> dict:
37
+ """
38
+ Deep merge two dictionaries.
39
+
40
+ Values from `override` take precedence. Nested dictionaries are merged recursively.
41
+
42
+ :param base: The base dictionary.
43
+ :param override: The dictionary with overriding values.
44
+ :return: A new dictionary with merged values.
45
+ """
46
+ result = base.copy()
47
+ for key, value in override.items():
48
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
49
+ result[key] = _deep_merge(result[key], value)
50
+ else:
51
+ result[key] = value
52
+ return result
53
+
54
+
36
55
  class CredentialType(TimeStampedModel):
37
56
  """
38
57
  Model to store global credential configurations for each type.
@@ -176,7 +195,7 @@ class CredentialConfiguration(TimeStampedModel):
176
195
  module = import_module(module_path)
177
196
  func = getattr(module, func_name)
178
197
 
179
- custom_options = {**self.credential_type.custom_options, **self.custom_options}
198
+ custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
180
199
  return func(self.learning_context_key, custom_options)
181
200
 
182
201
  def generate_credential_for_user(self, user_id: int, celery_task_id: int = 0):
@@ -195,7 +214,7 @@ class CredentialConfiguration(TimeStampedModel):
195
214
  # Use the name from the profile if it is not empty. Otherwise, use the first and last name.
196
215
  # We check if the profile exists because it may not exist in some cases (e.g., when a User is created manually).
197
216
  user_full_name = getattr(getattr(user, 'profile', None), 'name', f"{user.first_name} {user.last_name}")
198
- custom_options = {**self.credential_type.custom_options, **self.custom_options}
217
+ custom_options = _deep_merge(self.credential_type.custom_options, self.custom_options)
199
218
 
200
219
  credential, _ = Credential.objects.update_or_create(
201
220
  user_id=user_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: learning-credentials
3
- Version: 0.4.1rc5
3
+ Version: 0.4.1rc6
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "learning-credentials"
3
- version = "0.4.1rc5"
3
+ version = "0.4.1rc6"
4
4
  description = "A pluggable service for preparing Open edX credentials."
5
5
  dynamic = ["readme"]
6
6
  requires-python = ">=3.11"
@@ -160,6 +160,61 @@ class TestCredentialConfiguration:
160
160
  eligible_user_ids = self.config.get_eligible_user_ids()
161
161
  assert eligible_user_ids == [1, 2, 3]
162
162
 
163
+ @patch('test_models._mock_retrieval_func')
164
+ def test_custom_options_deep_merge(self, mock_retrieval_func: Mock):
165
+ """Test that custom_options are deep-merged between CredentialType and CredentialConfiguration."""
166
+ self.credential_type.custom_options = {
167
+ 'top_level': 'base_value',
168
+ 'nested': {
169
+ 'key1': 'base_key1',
170
+ 'key2': 'base_key2',
171
+ 'key3': {
172
+ 'sub_key1': 'sub_value1',
173
+ 'sub_key2': 'sub_value2',
174
+ },
175
+ 'key4': False,
176
+ 'key5': {
177
+ 'sub_key1': 'sub_value1',
178
+ 'sub_key2': 'sub_value2',
179
+ },
180
+ },
181
+ }
182
+ self.config.custom_options = {
183
+ 'top_level': 'override_value',
184
+ 'nested': {
185
+ 'key2': 'override_key2',
186
+ 'key6': 'new_key5',
187
+ 'key3': {
188
+ 'sub_key1': 'sub_override_value1',
189
+ },
190
+ 'key4': {
191
+ 'sub_key': 'sub_value',
192
+ },
193
+ 'key5': False,
194
+ },
195
+ 'new_top': 'new_value',
196
+ }
197
+
198
+ self.config.get_eligible_user_ids()
199
+
200
+ assert mock_retrieval_func.call_args[0][1] == {
201
+ 'top_level': 'override_value', # Overridden.
202
+ 'nested': {
203
+ 'key1': 'base_key1', # Preserved from type.
204
+ 'key2': 'override_key2', # Overridden.
205
+ 'key3': { # Merged into type, with an override.
206
+ 'sub_key1': 'sub_override_value1',
207
+ 'sub_key2': 'sub_value2',
208
+ },
209
+ 'key4': { # Overridden non-dict with dict.
210
+ 'sub_key': 'sub_value',
211
+ },
212
+ 'key5': False, # Overridden dict with non-dict.
213
+ 'key6': 'new_key5', # Added from override.
214
+ },
215
+ 'new_top': 'new_value', # Added from override.
216
+ }
217
+
163
218
  @pytest.mark.django_db
164
219
  def test_filter_out_user_ids_with_credentials(self):
165
220
  """Test the filter_out_user_ids_with_credentials method."""