Skip to content

Commit e8cf989

Browse files
committed
temp: moved draft processing over
1 parent b43d345 commit e8cf989

File tree

8 files changed

+144
-70
lines changed

8 files changed

+144
-70
lines changed

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,9 @@ def publish_from_drafts(
359359
# If the drafts include any containers, we need to auto-publish their descendants:
360360
# TODO: this only handles one level deep and would need to be updated to support sections > subsections > units
361361

362+
# note to self; manytomany through table for
363+
# dependencies = draft_qset.filter(dependency_set)
364+
362365
# Get the IDs of the ContainerVersion for any Containers whose drafts are slated to be published.
363366
container_version_ids = draft_qset.filter(entity__container__isnull=False) \
364367
.values_list("version_id", flat=True)
@@ -417,7 +420,7 @@ def publish_from_drafts(
417420
)
418421

419422
# Calculate the side-effects...
420-
_create_container_side_effects_for_publish_log(publish_log)
423+
_create_side_effects_for_publish_log(publish_log)
421424

422425
# Calculate state updates...
423426
_update_state_hash(publish_log)
@@ -562,7 +565,7 @@ def set_draft_version(
562565
old_version_id=old_version_id,
563566
new_version_id=publishable_entity_version_pk,
564567
)
565-
_create_container_side_effects_for_draft_change(change)
568+
_create_side_effects_for_draft_change(change)
566569

567570

568571
def _add_to_existing_draft_change_log(
@@ -628,7 +631,7 @@ def _add_to_existing_draft_change_log(
628631
return change
629632

630633

631-
def _create_container_side_effects_for_publish_log(publish_log: PublishLog):
634+
def _create_side_effects_for_publish_log(publish_log: PublishLog):
632635
"""
633636
Iterate through PublishLog and add the appropriate PublishSideEffects.
634637
@@ -711,24 +714,24 @@ def _create_container_side_effects_for_publish_log(publish_log: PublishLog):
711714
)
712715

713716

714-
def _create_container_side_effects_for_draft_change_log(change_log: DraftChangeLog):
717+
def _create_side_effects_for_draft_change_log(change_log: DraftChangeLog):
715718
"""
716719
Iterate through the whole DraftChangeLog and process side-effects.
717720
"""
718721
processed_entity_ids: set[int] = set()
719722
for change in change_log.records.all():
720-
_create_container_side_effects_for_draft_change(
723+
_create_side_effects_for_draft_change(
721724
change,
722725
processed_entity_ids=processed_entity_ids,
723726
)
724727

725728

726-
def _create_container_side_effects_for_draft_change(
729+
def _create_side_effects_for_draft_change(
727730
original_change: DraftChangeLogRecord,
728731
processed_entity_ids: set | None = None
729732
):
730733
"""
731-
Given a draft change, add side effects for all affected containers.
734+
Given a draft change, add side effects for all affected Drafts.
732735
733736
This should only be run after the DraftChangeLogRecord has been otherwise
734737
fully written out. We want to avoid the scenario where we create a
@@ -741,32 +744,34 @@ def _create_container_side_effects_for_draft_change(
741744
calling this function in a loop for all the Components in a Unit, we won't
742745
be recalculating the Unit's side-effect on its Subsection, and its
743746
Subsection's side-effect on its Section.
744-
745-
TODO: This could get very expensive with the get_containers_with_entity
746-
calls. We should measure the impact of this.
747747
"""
748748
if processed_entity_ids is None:
749749
# An optimization, but also a guard against infinite side-effect loops.
750750
processed_entity_ids = set()
751751

752-
changes_and_containers = [
753-
(original_change, container)
754-
for container
755-
in get_containers_with_entity(original_change.entity_id, ignore_pinned=True)
752+
# This is basically original_change.entity.draft.causes_side_effects_for,
753+
# but faster and safer (handles deleted drafts).
754+
target_dependencies = (
755+
DraftDependency.objects
756+
.filter(dependency_id=original_change.entity_id)
757+
.select_related("draft")
758+
)
759+
changes_and_side_effect_target_drafts = [
760+
(original_change, target_dep.target) for target_dep in target_dependencies
756761
]
757-
while changes_and_containers:
758-
change, container = changes_and_containers.pop()
762+
while changes_and_side_effect_target_drafts:
763+
change, draft = changes_and_side_effect_target_drafts.pop()
759764

760765
# If the container is not already in the DraftChangeLog, we need to
761766
# add it. Since it's being caused as a DraftSideEffect, we're going
762767
# add it with the old_version == new_version convention.
763-
container_draft_version_pk = container.versioning.draft.pk
764-
container_change, _created = DraftChangeLogRecord.objects.get_or_create(
768+
target_draft_version_pk = draft.version_id
769+
target_change, _created = DraftChangeLogRecord.objects.get_or_create(
765770
draft_change_log=change.draft_change_log,
766-
entity_id=container.pk,
771+
entity_id=draft.pk,
767772
defaults={
768-
'old_version_id': container_draft_version_pk,
769-
'new_version_id': container_draft_version_pk
773+
'old_version_id': target_draft_version_pk,
774+
'new_version_id': target_draft_version_pk
770775
}
771776
)
772777

@@ -776,20 +781,20 @@ def _create_container_side_effects_for_draft_change(
776781
# both the Unit and Component have their versions incremented, then the
777782
# Unit has changed in both ways (the Unit's internal metadata as well as
778783
# the new version of the child component).
779-
DraftSideEffect.objects.get_or_create(cause=change, effect=container_change)
784+
DraftSideEffect.objects.get_or_create(cause=change, effect=target_change)
780785
processed_entity_ids.add(change.entity_id)
781786

782787
# Now we find the next layer up of containers. So if the originally
783788
# passed in publishable_entity_id was for a Component, then the
784789
# ``container`` we've been creating the side effect for in this loop
785790
# is the Unit, and ``parents_of_container`` would be any Subsections
786791
# that contain the Unit.
787-
parents_of_container = get_containers_with_entity(container.pk, ignore_pinned=True)
792+
target_deps_of_draft = list(draft.causes_side_effects_for.all().select_related("draft"))
788793

789-
changes_and_containers.extend(
790-
(container_change, container_parent)
791-
for container_parent in parents_of_container
792-
if container_parent.pk not in processed_entity_ids
794+
changes_and_side_effect_target_drafts.extend(
795+
(target_change, target_dep.draft)
796+
for target_dep in target_deps_of_draft
797+
if target_dep.pk not in processed_entity_ids
793798
)
794799

795800

@@ -811,29 +816,16 @@ def set_draft_dependencies(draft: Draft, dependency_drafts: QuerySet[Draft]) ->
811816
like "all descendants" based on this.
812817
2. Declare dependencies from the bottom-up. In other words, if you're
813818
building an entire Subsection, set the Component dependencies for the
814-
Units before you set the Unit dependencies for the Subsection.
815-
3. Circular dependencies are not supported. It might not blow up when you
816-
set them, and it won't bring down the system when you do operations on
817-
them, but the behavior is undefined. See note below.
818-
819-
We're not currently guarding against the creation of circular dependencies
820-
because a) checking can become expensive; and b) it's an edge case that is
821-
unlikely to occur given the different types involved (e.g. Subsections can
822-
contain Units but Units can't contain Subsections).
823-
824-
We *will* guard against infinite loops in the actions we want to take with
825-
dependencies, like publishing them or calculating their side-effects.
826-
827-
That being said, we're defining the state for a Draft to be a function of
828-
the UUIDs + states of all of its dependencies, and making a circular
829-
dependency means that the state calculation will be incorrect and will
830-
probably result in weird behavior.
819+
Units before you set the Unit dependencies for the Subsection. This code
820+
will still work if you build from the top-down, but we'll end up doing
821+
many redundant re-calculations, since every change to a lower layer will
822+
cause recalculation to the higher levels that depend on it.
823+
3. Circular dependencies are not supported, and will raise a ``ValueError``.
831824
"""
832825
new_dependency_drafts = (
833826
dependency_drafts.exclude(pk=draft.pk) # draft can't depend on itself
834827
.distinct() # duplicates not allowed
835828
)
836-
new_state_hash = _calcuate_dependencies_state_hash(new_dependency_drafts)
837829

838830
# We're going to delete any DraftDependency rows that currently exist for
839831
# this draft but aren't represented in the new_dependency_drafts Draft qset.
@@ -849,7 +841,7 @@ def set_draft_dependencies(draft: Draft, dependency_drafts: QuerySet[Draft]) ->
849841
DraftDependency.objects.bulk_create(
850842
[
851843
DraftDependency(
852-
draft=draft,
844+
target=draft,
853845
dependency_id=dependency_draft.pk,
854846
version_at_creation_id=draft.version_id,
855847
)
@@ -859,7 +851,7 @@ def set_draft_dependencies(draft: Draft, dependency_drafts: QuerySet[Draft]) ->
859851
# because our value for "version_at_creation" will be incorrect.
860852
ignore_conflicts=True,
861853
)
862-
draft.state_hash = new_state_hash
854+
draft.state_hash = _calcuate_state_hash(draft)
863855

864856
# We've updated this Draft's state_hash based on its dependencies, but
865857
# now we have to re-calculate the state_hash for things that are
@@ -875,19 +867,23 @@ def set_draft_dependencies(draft: Draft, dependency_drafts: QuerySet[Draft]) ->
875867
"circular dependency."
876868
)
877869

878-
# Maybe always write the DraftDependency rows first so this function always works on one
879-
# queryset type?
880-
draft_with_outdated_state.state_hash = _calcuate_dependencies_state_hash(
881-
Draft.objects.filter(
882-
pk__in=draft_with_outdated_state.dependencies.all().values_list("draft_id", flat=True)
883-
)
870+
draft_with_outdated_state.state_hash = _calcuate_state_hash(draft_with_outdated_state)
871+
drafts_with_outdated_state.extend(
872+
dep.target for dep in draft_with_outdated_state.causes_side_effects_for.all().select_related("target")
884873
)
885874

886875

887-
def _calcuate_dependencies_state_hash(dependency_drafts: QuerySet[Draft]) -> str:
876+
def _calcuate_state_hash(draft: Draft) -> str:
888877
"""
889878
Generate a new value that can be put in Draft.state_hash.
890879
880+
We calculate the state hash based on the versions (UUIDs) and state_hash
881+
values of all of its dependencies. There is no sense of ordering with these
882+
dependencies. A Unit with Components [C1, C2] is the same to us as a Unit
883+
with [C2, C1], and both will have the same state_hash. We rely on the fact
884+
that containers store their ordering and that changes in that ordering mean
885+
that the version of the container itself (e.g. the Unit) changes.
886+
891887
Edge case: Deleted Drafts (i.e. draft.version == None)
892888
893889
When this happens, the deleted Draft still counts as a dependency because
@@ -898,13 +894,17 @@ def _calcuate_dependencies_state_hash(dependency_drafts: QuerySet[Draft]) -> str
898894
won't affect the top level Draft until the Unit is un-deleted, at which
899895
point we'd have to recalcuate the state_hash anyway.
900896
"""
901-
# We need to re-order for deterministic state calculation
902-
dependency_drafts = dependency_drafts.order_by("version__uuid").select_related("version")
903-
897+
dependencies: QuerySet[DraftDependency] = (
898+
draft.dependencies
899+
.all()
900+
.select_related("dependency__version")
901+
# need to guarantee order for deterministic state calculation
902+
.order_by("dependency__version__uuid")
903+
)
904904
dependency_states = [
905-
f"{dep_draft.version.uuid}:{dep_draft.state_hash}"
906-
for dep_draft in dependency_drafts
907-
if dep_draft.version # skip deleted drafts
905+
f"{dd.dependency.version.uuid}:{dd.dependency.state_hash}"
906+
for dd in dependencies
907+
if dd.dependency.version # skip deleted drafts
908908
]
909909
# Example line: [TODO: Fill this in]
910910
dependency_summary_text = "\n".join(dependency_states)
@@ -1689,6 +1689,6 @@ def bulk_draft_changes_for(
16891689
changed_at=changed_at,
16901690
changed_by=changed_by,
16911691
exit_callbacks=[
1692-
_create_container_side_effects_for_draft_change_log,
1692+
_create_side_effects_for_draft_change_log,
16931693
]
16941694
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 4.2.21 on 2025-06-07 00:56
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('oel_publishing', '0010_draft_dependency'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveConstraint(
14+
model_name='draftdependency',
15+
name='oel_pub_dd_uniq_draft_dep',
16+
),
17+
migrations.RenameField(
18+
model_name='draftdependency',
19+
old_name='draft',
20+
new_name='target',
21+
),
22+
migrations.AddConstraint(
23+
model_name='draftdependency',
24+
constraint=models.UniqueConstraint(fields=('target', 'dependency'), name='oel_pub_dd_uniq_target_dep'),
25+
),
26+
]

openedx_learning/apps/authoring/publishing/models/draft_log.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ class DraftDependency(models.Model):
348348
"""
349349
350350
"""
351-
draft = models.ForeignKey(
351+
target = models.ForeignKey(
352352
Draft,
353353
on_delete=models.CASCADE,
354354
related_name="dependencies",
@@ -369,7 +369,7 @@ class Meta:
369369
# Duplicate entries for a dependency are just redundant. This is
370370
# here to guard against weird bugs that might introduce this state.
371371
models.UniqueConstraint(
372-
fields=["draft", "dependency"],
373-
name="oel_pub_dd_uniq_draft_dep",
372+
fields=["target", "dependency"],
373+
name="oel_pub_dd_uniq_target_dep",
374374
)
375375
]

openedx_learning/apps/authoring/publishing/models/publish_log.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,6 @@ class Meta:
207207
]
208208
verbose_name = _("Publish Side Effect")
209209
verbose_name_plural = _("Publish Side Effects")
210+
211+
212+

tests/openedx_learning/apps/authoring/publishing/test_api.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,3 +1199,48 @@ def test_multiple_layers_of_containers(self):
11991199
assert unit_publish.affected_by.first().cause == component_publish
12001200
assert subsection_publish.affected_by.count() == 1
12011201
assert subsection_publish.affected_by.first().cause == unit_publish
1202+
1203+
def test_publish_all_layers(self):
1204+
"""Test that we can publish multiple layers from one root."""
1205+
# Note that these aren't real "components" and "units". Everything being
1206+
# tested is confined to the publishing app, so those concepts shouldn't
1207+
# be imported here. They're just named this way to make it more obvious
1208+
# what the intended hierarchy is for testing container nesting.
1209+
component = publishing_api.create_publishable_entity(
1210+
self.learning_package.id, "component_1", created=self.now, created_by=None,
1211+
)
1212+
publishing_api.create_publishable_entity_version(
1213+
component.id, version_num=1, title="Component 1 🌴", created=self.now, created_by=None,
1214+
)
1215+
unit = publishing_api.create_container(
1216+
self.learning_package.id, "unit_1", created=self.now, created_by=None,
1217+
)
1218+
publishing_api.create_container_version(
1219+
unit.pk,
1220+
1,
1221+
title="My Unit",
1222+
entity_rows=[
1223+
publishing_api.ContainerEntityRow(entity_pk=component.pk),
1224+
],
1225+
created=self.now,
1226+
created_by=None,
1227+
)
1228+
subsection = publishing_api.create_container(
1229+
self.learning_package.id, "subsection_1", created=self.now, created_by=None,
1230+
)
1231+
publishing_api.create_container_version(
1232+
subsection.pk,
1233+
1,
1234+
title="My Subsection",
1235+
entity_rows=[
1236+
publishing_api.ContainerEntityRow(entity_pk=unit.pk),
1237+
],
1238+
created=self.now,
1239+
created_by=None,
1240+
)
1241+
publish_log = publishing_api.publish_from_drafts(
1242+
self.learning_package.id,
1243+
Draft.objects.filter(pk=subsection.pk),
1244+
)
1245+
assert publish_log.records.count() == 3
1246+

tests/openedx_learning/apps/authoring/sections/test_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,9 @@ def test_create_section_queries(self):
214214
Test how many database queries are required to create a section
215215
"""
216216
# The exact numbers here aren't too important - this is just to alert us if anything significant changes.
217-
with self.assertNumQueries(27):
217+
with self.assertNumQueries(29):
218218
_empty_section = self.create_section_with_subsections([])
219-
with self.assertNumQueries(35):
219+
with self.assertNumQueries(36):
220220
# And try with a non-empty section:
221221
self.create_section_with_subsections([self.subsection_1, self.subsection_2_v1], key="u2")
222222

tests/openedx_learning/apps/authoring/subsections/test_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,9 @@ def test_create_subsection_queries(self):
197197
Test how many database queries are required to create a subsection
198198
"""
199199
# The exact numbers here aren't too important - this is just to alert us if anything significant changes.
200-
with self.assertNumQueries(27):
200+
with self.assertNumQueries(29):
201201
_empty_subsection = self.create_subsection_with_units([])
202-
with self.assertNumQueries(35):
202+
with self.assertNumQueries(36):
203203
# And try with a non-empty subsection:
204204
self.create_subsection_with_units([self.unit_1, self.unit_2_v1], key="u2")
205205

tests/openedx_learning/apps/authoring/units/test_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ def test_create_unit_queries(self):
185185
Test how many database queries are required to create a unit
186186
"""
187187
# The exact numbers here aren't too important - this is just to alert us if anything significant changes.
188-
with self.assertNumQueries(25):
188+
with self.assertNumQueries(27):
189189
_empty_unit = self.create_unit_with_components([])
190-
with self.assertNumQueries(32):
190+
with self.assertNumQueries(33):
191191
# And try with a non-empty unit:
192192
self.create_unit_with_components([self.component_1, self.component_2_v1], key="u2")
193193

0 commit comments

Comments
 (0)