openedx-learning 0.30.1__py2.py3-none-any.whl → 0.31.0__py2.py3-none-any.whl

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 (131) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/api/authoring.py +2 -9
  3. openedx_learning/api/authoring_models.py +7 -7
  4. openedx_learning/api/django.py +24 -0
  5. openedx_learning/apps/openedx_content/admin.py +13 -0
  6. openedx_learning/apps/openedx_content/api.py +16 -0
  7. openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/api.py +2 -2
  8. openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/serializers.py +1 -4
  9. openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/toml.py +4 -4
  10. openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/zipper.py +28 -14
  11. openedx_learning/apps/{authoring → openedx_content/applets}/collections/api.py +1 -1
  12. openedx_learning/apps/{authoring → openedx_content/applets}/collections/models.py +4 -1
  13. openedx_learning/apps/{authoring → openedx_content/applets}/components/admin.py +1 -1
  14. openedx_learning/apps/{authoring → openedx_content/applets}/components/api.py +1 -1
  15. openedx_learning/apps/{authoring → openedx_content/applets}/components/models.py +4 -2
  16. openedx_learning/apps/{authoring → openedx_content/applets}/contents/api.py +3 -2
  17. openedx_learning/apps/{authoring → openedx_content/applets}/contents/models.py +8 -2
  18. openedx_learning/apps/{authoring → openedx_content/applets}/publishing/api.py +1 -1
  19. openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/entity_list.py +1 -0
  20. openedx_learning/apps/{authoring → openedx_content/applets}/sections/api.py +1 -2
  21. openedx_learning/apps/{authoring → openedx_content/applets}/subsections/api.py +1 -2
  22. openedx_learning/apps/{authoring → openedx_content/applets}/units/api.py +1 -2
  23. openedx_learning/apps/openedx_content/apps.py +52 -0
  24. openedx_learning/apps/{authoring → openedx_content/backcompat}/backup_restore/apps.py +1 -1
  25. openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/apps.py +1 -1
  26. openedx_learning/apps/openedx_content/backcompat/collections/migrations/0006_remove_all_field_state_for_move_to_applet.py +82 -0
  27. openedx_learning/apps/openedx_content/backcompat/collections/models.py +9 -0
  28. openedx_learning/apps/openedx_content/backcompat/components/apps.py +15 -0
  29. openedx_learning/apps/openedx_content/backcompat/components/migrations/0005_remove_all_field_state_for_move_to_applet.py +72 -0
  30. openedx_learning/apps/openedx_content/backcompat/components/models.py +11 -0
  31. openedx_learning/apps/{authoring → openedx_content/backcompat}/contents/apps.py +1 -1
  32. openedx_learning/apps/openedx_content/backcompat/contents/migrations/0002_remove_all_field_state_for_move_to_applet.py +26 -0
  33. openedx_learning/apps/openedx_content/backcompat/publishing/apps.py +15 -0
  34. openedx_learning/apps/openedx_content/backcompat/publishing/migrations/0010_backfill_dependencies.py +432 -0
  35. openedx_learning/apps/openedx_content/backcompat/publishing/migrations/0011_remove_all_field_state_for_move_to_applet.py +288 -0
  36. openedx_learning/apps/openedx_content/backcompat/publishing/models.py +27 -0
  37. openedx_learning/apps/openedx_content/backcompat/sections/__init__.py +0 -0
  38. openedx_learning/apps/openedx_content/backcompat/sections/apps.py +16 -0
  39. openedx_learning/apps/openedx_content/backcompat/sections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  40. openedx_learning/apps/openedx_content/backcompat/sections/migrations/__init__.py +0 -0
  41. openedx_learning/apps/openedx_content/backcompat/subsections/__init__.py +0 -0
  42. openedx_learning/apps/openedx_content/backcompat/subsections/apps.py +16 -0
  43. openedx_learning/apps/openedx_content/backcompat/subsections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  44. openedx_learning/apps/openedx_content/backcompat/subsections/migrations/__init__.py +0 -0
  45. openedx_learning/apps/openedx_content/backcompat/units/__init__.py +0 -0
  46. openedx_learning/apps/openedx_content/backcompat/units/apps.py +16 -0
  47. openedx_learning/apps/openedx_content/backcompat/units/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  48. openedx_learning/apps/openedx_content/backcompat/units/migrations/__init__.py +0 -0
  49. openedx_learning/apps/openedx_content/management/__init__.py +0 -0
  50. openedx_learning/apps/openedx_content/management/commands/__init__.py +0 -0
  51. openedx_learning/apps/{authoring/components → openedx_content}/management/commands/add_assets_to_component.py +1 -2
  52. openedx_learning/apps/{authoring/backup_restore → openedx_content}/management/commands/lp_dump.py +2 -2
  53. openedx_learning/apps/{authoring/backup_restore → openedx_content}/management/commands/lp_load.py +1 -1
  54. openedx_learning/apps/openedx_content/migrations/0001_initial.py +654 -0
  55. openedx_learning/apps/openedx_content/migrations/0002_rename_tables_to_openedx_content.py +138 -0
  56. openedx_learning/apps/openedx_content/migrations/__init__.py +0 -0
  57. openedx_learning/apps/openedx_content/models.py +17 -0
  58. openedx_learning/contrib/media_server/views.py +1 -1
  59. {openedx_learning-0.30.1.dist-info → openedx_learning-0.31.0.dist-info}/METADATA +6 -6
  60. openedx_learning-0.31.0.dist-info/RECORD +194 -0
  61. {openedx_learning-0.30.1.dist-info → openedx_learning-0.31.0.dist-info}/WHEEL +1 -1
  62. openedx_learning/apps/authoring/components/apps.py +0 -24
  63. openedx_learning/apps/authoring/publishing/apps.py +0 -25
  64. openedx_learning/apps/authoring/publishing/migrations/0010_backfill_dependencies.py +0 -149
  65. openedx_learning/apps/authoring/sections/apps.py +0 -25
  66. openedx_learning/apps/authoring/subsections/apps.py +0 -25
  67. openedx_learning/apps/authoring/units/apps.py +0 -25
  68. openedx_learning-0.30.1.dist-info/RECORD +0 -168
  69. /openedx_learning/apps/{authoring → openedx_content}/__init__.py +0 -0
  70. /openedx_learning/apps/{authoring/backup_restore → openedx_content/applets}/__init__.py +0 -0
  71. /openedx_learning/apps/{authoring/backup_restore/management → openedx_content/applets/backup_restore}/__init__.py +0 -0
  72. /openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/admin.py +0 -0
  73. /openedx_learning/apps/{authoring → openedx_content/applets}/backup_restore/models.py +0 -0
  74. /openedx_learning/apps/{authoring/backup_restore/management/commands → openedx_content/applets/collections}/__init__.py +0 -0
  75. /openedx_learning/apps/{authoring → openedx_content/applets}/collections/admin.py +0 -0
  76. /openedx_learning/apps/{authoring/backup_restore/migrations → openedx_content/applets/components}/__init__.py +0 -0
  77. /openedx_learning/apps/{authoring/collections → openedx_content/applets/contents}/__init__.py +0 -0
  78. /openedx_learning/apps/{authoring → openedx_content/applets}/contents/admin.py +0 -0
  79. /openedx_learning/apps/{authoring/collections/migrations → openedx_content/applets/publishing}/__init__.py +0 -0
  80. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/admin.py +0 -0
  81. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/contextmanagers.py +0 -0
  82. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/__init__.py +0 -0
  83. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/container.py +0 -0
  84. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/draft_log.py +0 -0
  85. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/learning_package.py +0 -0
  86. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/publish_log.py +0 -0
  87. /openedx_learning/apps/{authoring → openedx_content/applets}/publishing/models/publishable_entity.py +0 -0
  88. /openedx_learning/apps/{authoring/components → openedx_content/applets/sections}/__init__.py +0 -0
  89. /openedx_learning/apps/{authoring → openedx_content/applets}/sections/admin.py +0 -0
  90. /openedx_learning/apps/{authoring → openedx_content/applets}/sections/models.py +0 -0
  91. /openedx_learning/apps/{authoring/components/management → openedx_content/applets/subsections}/__init__.py +0 -0
  92. /openedx_learning/apps/{authoring → openedx_content/applets}/subsections/admin.py +0 -0
  93. /openedx_learning/apps/{authoring → openedx_content/applets}/subsections/models.py +0 -0
  94. /openedx_learning/apps/{authoring/components/management/commands → openedx_content/applets/units}/__init__.py +0 -0
  95. /openedx_learning/apps/{authoring → openedx_content/applets}/units/admin.py +0 -0
  96. /openedx_learning/apps/{authoring → openedx_content/applets}/units/models.py +0 -0
  97. /openedx_learning/apps/{authoring/components/migrations → openedx_content/backcompat}/__init__.py +0 -0
  98. /openedx_learning/apps/{authoring/contents → openedx_content/backcompat/backup_restore}/__init__.py +0 -0
  99. /openedx_learning/apps/{authoring/contents → openedx_content/backcompat/backup_restore}/migrations/__init__.py +0 -0
  100. /openedx_learning/apps/{authoring/publishing → openedx_content/backcompat/collections}/__init__.py +0 -0
  101. /openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/migrations/0001_initial.py +0 -0
  102. /openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +0 -0
  103. /openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/migrations/0003_collection_entities.py +0 -0
  104. /openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/migrations/0004_collection_key.py +0 -0
  105. /openedx_learning/apps/{authoring → openedx_content/backcompat}/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +0 -0
  106. /openedx_learning/apps/{authoring/publishing → openedx_content/backcompat/collections}/migrations/__init__.py +0 -0
  107. /openedx_learning/apps/{authoring/sections → openedx_content/backcompat/components}/__init__.py +0 -0
  108. /openedx_learning/apps/{authoring → openedx_content/backcompat}/components/migrations/0001_initial.py +0 -0
  109. /openedx_learning/apps/{authoring → openedx_content/backcompat}/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
  110. /openedx_learning/apps/{authoring → openedx_content/backcompat}/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +0 -0
  111. /openedx_learning/apps/{authoring → openedx_content/backcompat}/components/migrations/0004_remove_componentversioncontent_uuid.py +0 -0
  112. /openedx_learning/apps/{authoring/sections → openedx_content/backcompat/components}/migrations/__init__.py +0 -0
  113. /openedx_learning/apps/{authoring/subsections → openedx_content/backcompat/contents}/__init__.py +0 -0
  114. /openedx_learning/apps/{authoring → openedx_content/backcompat}/contents/migrations/0001_initial.py +0 -0
  115. /openedx_learning/apps/{authoring/subsections → openedx_content/backcompat/contents}/migrations/__init__.py +0 -0
  116. /openedx_learning/apps/{authoring/units → openedx_content/backcompat/publishing}/__init__.py +0 -0
  117. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0001_initial.py +0 -0
  118. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
  119. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0003_containers.py +0 -0
  120. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0004_publishableentity_can_stand_alone.py +0 -0
  121. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0005_alter_entitylistrow_options.py +0 -0
  122. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0006_draftchangelog.py +0 -0
  123. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0007_bootstrap_draftchangelog.py +0 -0
  124. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +0 -0
  125. /openedx_learning/apps/{authoring → openedx_content/backcompat}/publishing/migrations/0009_dependencies_and_hashing.py +0 -0
  126. /openedx_learning/apps/{authoring/units → openedx_content/backcompat/publishing}/migrations/__init__.py +0 -0
  127. /openedx_learning/apps/{authoring → openedx_content/backcompat}/sections/migrations/0001_initial.py +0 -0
  128. /openedx_learning/apps/{authoring → openedx_content/backcompat}/subsections/migrations/0001_initial.py +0 -0
  129. /openedx_learning/apps/{authoring → openedx_content/backcompat}/units/migrations/0001_initial.py +0 -0
  130. {openedx_learning-0.30.1.dist-info → openedx_learning-0.31.0.dist-info}/licenses/LICENSE.txt +0 -0
  131. {openedx_learning-0.30.1.dist-info → openedx_learning-0.31.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,15 @@
1
+ """
2
+ Django metadata for the Components Django application.
3
+ """
4
+ from django.apps import AppConfig
5
+
6
+
7
+ class ComponentsConfig(AppConfig):
8
+ """
9
+ Configuration for the Components Django application.
10
+ """
11
+
12
+ name = "openedx_learning.apps.openedx_content.backcompat.components"
13
+ verbose_name = "Learning Core > Authoring > Components"
14
+ default_auto_field = "django.db.models.BigAutoField"
15
+ label = "oel_components"
@@ -0,0 +1,72 @@
1
+ # Generated by Django 5.2.10 on 2026-01-30 00:36
2
+
3
+ from django.db import migrations
4
+ from django.db.migrations.operations.special import SeparateDatabaseAndState
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('oel_components', '0004_remove_componentversioncontent_uuid'),
11
+ ]
12
+
13
+ operations = [
14
+ SeparateDatabaseAndState(
15
+ database_operations=[],
16
+ state_operations=[
17
+ migrations.RemoveField(
18
+ model_name='component',
19
+ name='component_type',
20
+ ),
21
+ migrations.RemoveField(
22
+ model_name='componentversion',
23
+ name='component',
24
+ ),
25
+ migrations.RemoveField(
26
+ model_name='componentversion',
27
+ name='contents',
28
+ ),
29
+ migrations.RemoveField(
30
+ model_name='componentversion',
31
+ name='publishable_entity_version',
32
+ ),
33
+ migrations.RemoveField(
34
+ model_name='componentversioncontent',
35
+ name='component_version',
36
+ ),
37
+ migrations.RemoveField(
38
+ model_name='componentversioncontent',
39
+ name='content',
40
+ ),
41
+ migrations.AlterModelOptions(
42
+ name='component',
43
+ options={},
44
+ ),
45
+ migrations.RemoveConstraint(
46
+ model_name='component',
47
+ name='oel_component_uniq_lc_ct_lk',
48
+ ),
49
+ migrations.RemoveIndex(
50
+ model_name='component',
51
+ name='oel_component_idx_ct_lk',
52
+ ),
53
+ migrations.RemoveField(
54
+ model_name='component',
55
+ name='learning_package',
56
+ ),
57
+ migrations.RemoveField(
58
+ model_name='component',
59
+ name='local_key',
60
+ ),
61
+ migrations.DeleteModel(
62
+ name='ComponentType',
63
+ ),
64
+ migrations.DeleteModel(
65
+ name='ComponentVersion',
66
+ ),
67
+ migrations.DeleteModel(
68
+ name='ComponentVersionContent',
69
+ ),
70
+ ]
71
+ ),
72
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ These are stub models necessary to ensure a smooth migration for
3
+ openedx-platform apps that were built to have foreign keys to these models.
4
+ """
5
+ from django.db import models
6
+
7
+
8
+ class Component(models.Model):
9
+ publishable_entity = models.OneToOneField(
10
+ 'oel_publishing.PublishableEntity', on_delete=models.CASCADE, primary_key=True
11
+ )
@@ -9,7 +9,7 @@ class ContentsConfig(AppConfig):
9
9
  Configuration for the Contents Django application.
10
10
  """
11
11
 
12
- name = "openedx_learning.apps.authoring.contents"
12
+ name = "openedx_learning.apps.openedx_content.backcompat.contents"
13
13
  verbose_name = "Learning Core > Authoring > Contents"
14
14
  default_auto_field = "django.db.models.BigAutoField"
15
15
  label = "oel_contents"
@@ -0,0 +1,26 @@
1
+ # Generated by Django 5.2.10 on 2026-01-30 00:36
2
+
3
+ from django.db import migrations
4
+ from django.db.migrations.operations.special import SeparateDatabaseAndState
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('oel_components', '0005_remove_all_field_state_for_move_to_applet'),
11
+ ('oel_contents', '0001_initial'),
12
+ ]
13
+
14
+ operations = [
15
+ SeparateDatabaseAndState(
16
+ database_operations=[],
17
+ state_operations=[
18
+ migrations.DeleteModel(
19
+ name='Content',
20
+ ),
21
+ migrations.DeleteModel(
22
+ name='MediaType',
23
+ ),
24
+ ]
25
+ ),
26
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ Publishing Django application initialization.
3
+ """
4
+ from django.apps import AppConfig
5
+
6
+
7
+ class PublishingConfig(AppConfig):
8
+ """
9
+ Configuration for the publishing Django application.
10
+ """
11
+
12
+ name = "openedx_learning.apps.openedx_content.backcompat.publishing"
13
+ verbose_name = "Learning Core > Authoring > Publishing"
14
+ default_auto_field = "django.db.models.BigAutoField"
15
+ label = "oel_publishing"
@@ -0,0 +1,432 @@
1
+ """
2
+ Backfill PublishableEntityVersionDependency entries based on ContainerVersions.
3
+
4
+ We're introducing a lower-level publishing concept of a dependency that will be
5
+ used by Containers, but this means we have to backfill that dependency info for
6
+ existing Containers in the system.
7
+
8
+ For more information, please see the PublishableEntityVersionDependency model.
9
+
10
+ Note that we copy a *lot* of API code here because we want to make sure that
11
+ this migration continues to work on the historical version of the models at this
12
+ point in time, regardless of any future changes that might be made to the API
13
+ for calculating dependencies. Most of the API code here is copy-pasted from the
14
+ publishing API, with very minor changes to grab the historical models using
15
+ apps.get_model(), rather than importing the models directly.
16
+ """
17
+ from django.db import migrations
18
+ from django.db.models import F, Prefetch
19
+
20
+ from openedx_learning.lib.fields import create_hash_digest
21
+
22
+
23
+ def create_backfill(apps, schema_editor):
24
+ """
25
+ Create dependency entries and update dep hashes for Draft and Published.
26
+ """
27
+ _create_dependencies(apps)
28
+ _update_drafts(apps)
29
+ _update_draft_dependencies_hashes(apps)
30
+ _update_published_dependencies_hashes(apps)
31
+
32
+
33
+ def _create_dependencies(apps):
34
+ """
35
+ Populate the PublishableEntityVersion.dependencies relation.
36
+
37
+ The only ones we should have in the system at this point are the ones from
38
+ containers, so we query ContainerVersion for that.
39
+ """
40
+ PublishableEntityVersionDependency = apps.get_model(
41
+ "oel_publishing", "PublishableEntityVersionDependency"
42
+ )
43
+ ContainerVersion = apps.get_model("oel_publishing", "ContainerVersion")
44
+
45
+ for container_version in ContainerVersion.objects.all():
46
+ # child_entity_ids is a set to de-dupe. This doesn't handle pinned
47
+ # child references yet, but you can't actually make those in a real
48
+ # library yet, so we shouldn't have that data lying around to migrate.
49
+ child_entity_ids = set(
50
+ container_version
51
+ .entity_list
52
+ .entitylistrow_set
53
+ .all()
54
+ .values_list("entity_id", flat=True)
55
+ )
56
+ PublishableEntityVersionDependency.objects.bulk_create(
57
+ [
58
+ PublishableEntityVersionDependency(
59
+ referring_version_id=container_version.pk,
60
+ referenced_entity_id=entity_id
61
+ )
62
+ for entity_id in child_entity_ids
63
+ ],
64
+ ignore_conflicts=True,
65
+ )
66
+
67
+
68
+ def _update_drafts(apps):
69
+ """
70
+ Update Draft entries to point to their most recent DraftLogRecord.
71
+
72
+ This is slow and expensive.
73
+ """
74
+ Draft = apps.get_model("oel_publishing", "Draft")
75
+ DraftChangeLogRecord = apps.get_model("oel_publishing", "DraftChangeLogRecord")
76
+ for draft in Draft.objects.all():
77
+ draft_log_record = (
78
+ # Find the most recent DraftChangeLogRecord related to this Draft,
79
+ DraftChangeLogRecord.objects
80
+ .filter(entity_id=draft.pk)
81
+ .order_by('-pk')
82
+ .first()
83
+ )
84
+ draft.draft_log_record = draft_log_record
85
+ draft.save()
86
+
87
+
88
+ def _update_draft_dependencies_hashes(apps):
89
+ """
90
+ Update the dependency_hash_digest for all DraftChangeLogRecords.
91
+
92
+ Backfill dependency state hashes. The important thing here is that things
93
+ without dependencies will have the default (blank) state hash, so we only
94
+ need to query for Draft entries for Containers.
95
+
96
+ We are only backfilling the current DraftChangeLogRecords pointed to by the
97
+ Draft entries now. We are not backfilling all historical
98
+ DraftChangeLogRecords. Full historical reconstruction is probably possible,
99
+ but it's not really worth the cost and complexity.
100
+ """
101
+ DraftChangeLog = apps.get_model("oel_publishing", "DraftChangeLog")
102
+
103
+ # All DraftChangeLogs that have records that are pointed to by the current
104
+ # Draft and have a possibility of having dependencies.
105
+ change_logs = DraftChangeLog.objects.filter(
106
+ pk=F('records__entity__draft__draft_log_record__draft_change_log'),
107
+ records__entity__draft__version__isnull=False,
108
+ records__entity__container__isnull=False,
109
+ ).distinct()
110
+ for change_log in change_logs:
111
+ update_dependencies_hash_digests_for_log(change_log, apps)
112
+
113
+ def _update_published_dependencies_hashes(apps):
114
+ """
115
+ Update all container Published.dependencies_hash_digest
116
+
117
+ Backfill dependency state hashes. The important thing here is that things
118
+ without dependencies will have the default (blank) state hash, so we only
119
+ need to query for Published entries for Containers.
120
+ """
121
+ PublishLog = apps.get_model("oel_publishing", "PublishLog")
122
+
123
+ # All PublishLogs that have records that are pointed to by the current
124
+ # Published and have a possibility of having dependencies.
125
+ change_logs = PublishLog.objects.filter(
126
+ pk=F('records__entity__published__publish_log_record__publish_log'),
127
+ records__entity__published__version__isnull=False,
128
+ records__entity__container__isnull=False,
129
+ ).distinct()
130
+ for change_log in change_logs:
131
+ update_dependencies_hash_digests_for_log(change_log, apps)
132
+
133
+ def remove_backfill(apps, schema_editor):
134
+ """
135
+ Reset all dep hash values to default ('') and remove dependencies.
136
+ """
137
+ Draft = apps.get_model("oel_publishing", "Draft")
138
+ DraftChangeLogRecord = apps.get_model("oel_publishing", "DraftChangeLogRecord")
139
+ PublishLogRecord = apps.get_model("oel_publishing", "PublishLogRecord")
140
+ PublishableEntityVersionDependency = apps.get_model(
141
+ "oel_publishing", "PublishableEntityVersionDependency"
142
+ )
143
+
144
+ PublishLogRecord.objects.all().update(dependencies_hash_digest='')
145
+ DraftChangeLogRecord.objects.all().update(dependencies_hash_digest='')
146
+ PublishableEntityVersionDependency.objects.all().delete()
147
+ Draft.objects.all().update(draft_log_record=None)
148
+
149
+
150
+ def update_dependencies_hash_digests_for_log(
151
+ change_log, # this is a historical DraftChangeLog or PublishLog
152
+ apps,
153
+ backfill=True,
154
+ ) -> None:
155
+ """
156
+ Update dependencies_hash_digest for Drafts or Published in a change log.
157
+
158
+ This is copied from the publishing API to make sure we don't accidentally
159
+ break it with future changes as the data model evolves. It has also been
160
+ modified to use historical models, rather than having references to the new
161
+ ones that have been moved to the centralized authoring app. It has also been
162
+ modified to assume that it's being used as a backfill (the original makes it
163
+ optional).
164
+
165
+ All the data for Draft/Published, DraftChangeLog/PublishLog, and
166
+ DraftChangeLogRecord/PublishLogRecord have been set at this point *except*
167
+ the dependencies_hash_digest of DraftChangeLogRecord/PublishLogRecord. Those
168
+ log records are newly created at this point, so dependencies_hash_digest are
169
+ set to their default values.
170
+
171
+ Args:
172
+ change_log: A DraftChangeLog or PublishLog that already has all
173
+ side-effects added to it. The Draft and Published models should
174
+ already be updated to point to the post-change versions.
175
+ backfill: If this is true, we will not trust the hash values stored on
176
+ log records outside of our log, i.e. things that we would normally
177
+ expect to be pre-calculated. This will be important for the initial
178
+ data migration.
179
+ """
180
+ DraftChangeLog = apps.get_model("oel_publishing", "DraftChangeLog")
181
+ DraftChangeLogRecord = apps.get_model("oel_publishing", "DraftChangeLogRecord")
182
+ PublishLog = apps.get_model("oel_publishing", "PublishLog")
183
+ PublishLogRecord = apps.get_model("oel_publishing", "PublishLogRecord")
184
+ PublishableEntity = apps.get_model("oel_publishing", "PublishableEntity")
185
+
186
+ if isinstance(change_log, DraftChangeLog):
187
+ branch = "draft"
188
+ log_record_relation = "draft_log_record"
189
+ record_cls = DraftChangeLogRecord
190
+ elif isinstance(change_log, PublishLog):
191
+ branch = "published"
192
+ log_record_relation = "publish_log_record"
193
+ record_cls = PublishLogRecord # type: ignore[assignment]
194
+ else:
195
+ raise TypeError(
196
+ f"expected DraftChangeLog or PublishLog, not {type(change_log)}"
197
+ )
198
+
199
+ dependencies_prefetch = Prefetch(
200
+ "new_version__dependencies",
201
+ queryset=PublishableEntity.objects
202
+ .select_related(
203
+ f"{branch}__version",
204
+ f"{branch}__{log_record_relation}",
205
+ )
206
+ .order_by(f"{branch}__version__uuid")
207
+ )
208
+ changed_records = (
209
+ change_log.records
210
+ .select_related("new_version", f"entity__{branch}")
211
+ .prefetch_related(dependencies_prefetch)
212
+ )
213
+
214
+ record_ids_to_hash_digests: dict[int, str | None] = {}
215
+ record_ids_to_live_deps: dict[int, list] = {}
216
+ records_that_need_hashes = []
217
+
218
+ for record in changed_records:
219
+ # This is a soft-deletion, so the dependency hash is default/blank. We
220
+ # set this value in our record_ids_to_hash_digests cache, but we don't
221
+ # need to write it to the database because it's just the default value.
222
+ if record.new_version is None:
223
+ record_ids_to_hash_digests[record.id] = ''
224
+ continue
225
+
226
+ # Now check to see if the new version has "live" dependencies, i.e.
227
+ # dependencies that have not been deleted.
228
+ deps = list(
229
+ entity for entity in record.new_version.dependencies.all()
230
+ if hasattr(entity, branch) and getattr(entity, branch).version
231
+ )
232
+
233
+ # If there are no live dependencies, this log record also gets the
234
+ # default/blank value.
235
+ if not deps:
236
+ record_ids_to_hash_digests[record.id] = ''
237
+ continue
238
+
239
+ # If we've gotten this far, it means that this record has dependencies
240
+ # and does need to get a hash computed for it.
241
+ records_that_need_hashes.append(record)
242
+ record_ids_to_live_deps[record.id] = deps
243
+
244
+ if backfill:
245
+ untrusted_record_id_set = None
246
+ else:
247
+ untrusted_record_id_set = set(rec.id for rec in records_that_need_hashes)
248
+
249
+ for record in records_that_need_hashes:
250
+ record.dependencies_hash_digest = hash_for_log_record(
251
+ apps,
252
+ record,
253
+ record_ids_to_hash_digests,
254
+ record_ids_to_live_deps,
255
+ untrusted_record_id_set,
256
+ )
257
+
258
+ _bulk_update_hashes(record_cls, records_that_need_hashes)
259
+
260
+
261
+ def _bulk_update_hashes(model_cls, records):
262
+ """
263
+ bulk_update using the model class (PublishLogRecord or DraftChangeLogRecord)
264
+
265
+ This is copied from the publishing API to make sure we don't accidentally
266
+ break it with future changes as the data model evolves.
267
+ """
268
+ model_cls.objects.bulk_update(records, ['dependencies_hash_digest'])
269
+
270
+
271
+ def hash_for_log_record(
272
+ apps,
273
+ record, # historical DraftChangeLogRecord | PublishLogRecord,
274
+ record_ids_to_hash_digests: dict,
275
+ record_ids_to_live_deps: dict,
276
+ untrusted_record_id_set: set | None,
277
+ ) -> str:
278
+ """
279
+ The hash digest for a given change log record.
280
+
281
+ This is copied from the publishing API to make sure we don't accidentally
282
+ break it with future changes as the data model evolves. It has also been
283
+ modified to use historical models, rather than having references to the new
284
+ ones that have been moved to the centralized authoring app.
285
+
286
+ Note that this code is a little convoluted because we're working hard to
287
+ minimize the number of database requests. All the data we really need could
288
+ be derived from querying various relations off the record that's passed in
289
+ as the first parameter, but at a far higher cost.
290
+
291
+ The hash calculated here will be used for the dependencies_hash_digest
292
+ attribute of DraftChangeLogRecord and PublishLogRecord. The hash is intended
293
+ to calculate the currently "live" (current draft or published) state of all
294
+ dependencies (and transitive dependencies) of the PublishableEntityVersion
295
+ pointed to by DraftChangeLogRecord.new_version/PublishLogRecord.new_version.
296
+
297
+ The common case we have at the moment is when a container type like a Unit
298
+ has unpinned child Components as dependencies. In the data model, those
299
+ dependency relationships are represented by the "dependencies" M:M relation
300
+ on PublishableEntityVersion. Since the Unit version's references to its
301
+ child Components are unpinned, the draft Unit is always pointing to the
302
+ latest draft versions of those Components and the published Unit is always
303
+ pointing to the latest published versions of those Components.
304
+
305
+ This means that the total draft or published state of any PublishableEntity
306
+ depends on the combination of:
307
+
308
+ 1. The definition of the current draft/published version of that entity.
309
+ Example: Version 1 of a Unit would define that it had children [C1, C2].
310
+ Version 2 of the same Unit might have children [C1, C2, C3].
311
+ 2. The current draft/published versions of all dependencies. Example: What
312
+ are the current draft and published versions of C1, C2, and C3.
313
+
314
+ This is why it makes sense to capture in a log record, since
315
+ PublishLogRecords or DraftChangeLogRecords are created whenever one of the
316
+ above two things changes.
317
+
318
+ Here are the possible scenarios, including edge cases:
319
+
320
+ EntityVersions with no dependencies
321
+ If record.new_version has no dependencies, dependencies_hash_digest is
322
+ set to the default value of ''. This will be the most common case.
323
+
324
+ EntityVersions with dependencies
325
+ If an EntityVersion has dependencies, then its draft/published state
326
+ hash is based on the concatenation of, for each non-deleted dependency:
327
+ (i) the dependency's draft/published EntityVersion primary key, and
328
+ (ii) the dependency's own draft/published state hash, recursively re-
329
+ calculated if necessary.
330
+
331
+ Soft-deletions
332
+ If the record.new_version is None, that means we've just soft-deleted
333
+ something (or published the soft-delete of something). We adopt the
334
+ convention that if something is soft-deleted, its dependencies_hash_digest
335
+ is reset to the default value of ''. This is not strictly necessary for
336
+ the recursive hash calculation, but deleted entities will not have their
337
+ hash updated even as their non-deleted dependencies are updated underneath
338
+ them, so we set to '' to avoid falsely implying that the deleted entity's
339
+ dep hash is up to date.
340
+
341
+ EntityVersions with soft-deleted dependencies
342
+ A soft-deleted dependency isn't counted (it's as if the dependency were
343
+ removed). If all of an EntityVersion's dependencies are soft-deleted,
344
+ then it will go back to having to having the default blank string for its
345
+ dependencies_hash_digest.
346
+ """
347
+ DraftChangeLogRecord = apps.get_model("oel_publishing", "DraftChangeLogRecord")
348
+ PublishLogRecord = apps.get_model("oel_publishing", "PublishLogRecord")
349
+
350
+ # Case #1: We've already computed this, or it was bootstrapped for us in the
351
+ # cache because the record is a deletion or doesn't have dependencies.
352
+ if record.id in record_ids_to_hash_digests:
353
+ return record_ids_to_hash_digests[record.id]
354
+
355
+ # Case #2: The log_record is a dependency of something that was affected by
356
+ # a change, but the dependency itself did not change in any way (neither
357
+ # directly, nor as a side-effect).
358
+ #
359
+ # Example: A Unit has two Components. One of the Components changed, forcing
360
+ # us to recalculate the dependencies_hash_digest for that Unit. Doing that
361
+ # recalculation requires us to fetch the dependencies_hash_digest of the
362
+ # unchanged child Component as well.
363
+ #
364
+ # If we aren't given an explicit untrusted_record_id_set, it means we can't
365
+ # trust anything. This would happen when we're bootstrapping things with an
366
+ # initial data migration.
367
+ if (untrusted_record_id_set is not None) and (record.id not in untrusted_record_id_set):
368
+ return record.dependencies_hash_digest
369
+
370
+ # Normal recursive case starts here:
371
+ if isinstance(record, DraftChangeLogRecord):
372
+ branch = "draft"
373
+ elif isinstance(record, PublishLogRecord):
374
+ branch = "published"
375
+ else:
376
+ raise TypeError(
377
+ f"expected DraftChangeLogRecord or PublishLogRecord, not {type(record)}"
378
+ )
379
+
380
+ # This is extra work that only happens in case of a backfill, where we might
381
+ # need to compute dependency hashes for things outside of our log (because
382
+ # we don't trust them).
383
+ if record.id not in record_ids_to_live_deps:
384
+ if record.new_version is None:
385
+ record_ids_to_hash_digests[record.id] = ''
386
+ return ''
387
+ deps = list(
388
+ entity for entity in record.new_version.dependencies.all()
389
+ if hasattr(entity, branch) and getattr(entity, branch).version
390
+ )
391
+ # If there are no live dependencies, this log record also gets the
392
+ # default/blank value.
393
+ if not deps:
394
+ record_ids_to_hash_digests[record.id] = ''
395
+ return ''
396
+
397
+ record_ids_to_live_deps[record.id] = deps
398
+ # End special handling for backfill.
399
+
400
+ # Begin normal
401
+ dependencies = sorted(
402
+ record_ids_to_live_deps[record.id],
403
+ key=lambda entity: getattr(entity, branch).log_record.new_version_id,
404
+ )
405
+ dep_state_entries = []
406
+ for dep_entity in dependencies:
407
+ new_version_id = getattr(dep_entity, branch).log_record.new_version_id
408
+ hash_digest = hash_for_log_record(
409
+ apps,
410
+ getattr(dep_entity, branch).log_record,
411
+ record_ids_to_hash_digests,
412
+ record_ids_to_live_deps,
413
+ untrusted_record_id_set,
414
+ )
415
+ dep_state_entries.append(f"{new_version_id}:{hash_digest}")
416
+ summary_text = "\n".join(dep_state_entries)
417
+
418
+ digest = create_hash_digest(summary_text.encode(), num_bytes=4)
419
+ record_ids_to_hash_digests[record.id] = digest
420
+
421
+ return digest
422
+
423
+
424
+ class Migration(migrations.Migration):
425
+
426
+ dependencies = [
427
+ ('oel_publishing', '0009_dependencies_and_hashing'),
428
+ ]
429
+
430
+ operations = [
431
+ migrations.RunPython(create_backfill, reverse_code=remove_backfill)
432
+ ]