@@ -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
568571def _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 )
0 commit comments