diff --git a/src/openedx_content/applets/publishing/api.py b/src/openedx_content/applets/publishing/api.py index 9494bfcf1..0a51ea05f 100644 --- a/src/openedx_content/applets/publishing/api.py +++ b/src/openedx_content/applets/publishing/api.py @@ -474,6 +474,10 @@ def publish_from_drafts( By default, this will also publish all dependencies (e.g. unpinned children) of the Drafts that are passed in. """ + if DraftChangeLogContext.get_active_draft_change_log(learning_package_id) is not None: + raise ValidationError( + f"Cannot publish learning package {learning_package_id} while in bulk_draft_changes_for()." + ) if published_at is None: published_at = datetime.now(tz=timezone.utc) diff --git a/tests/openedx_content/applets/publishing/test_api.py b/tests/openedx_content/applets/publishing/test_api.py index 157c091a7..5c82edd1a 100644 --- a/tests/openedx_content/applets/publishing/test_api.py +++ b/tests/openedx_content/applets/publishing/test_api.py @@ -2533,3 +2533,47 @@ def test_create_version_rejects_cross_package_dependencies(self) -> None: created_by=None, dependencies=[entity_in_lp2.id], ) + + def test_publish_functions_rejected_inside_bulk_draft_changes_for(self) -> None: + """ + publish_all_drafts() and publish_from_drafts() must not be callable + from within a bulk_draft_changes_for() context. + + bulk_draft_changes_for() opens a DraftChangeLog for accumulating draft + edits; running a publish inside it mixes draft-change bookkeeping with + publish bookkeeping in the same atomic block, which corrupts the + ordering of DraftChangeLog vs. PublishLog records and can leave Drafts + and Published rows out of sync if the outer context later raises. + """ + lp1_id = self.learning_package_1.id + entity = publishing_api.create_publishable_entity( + self.learning_package_1.id, + "entity_for_bulk_publish_check", + created=self.now, + created_by=None, + ) + publishing_api.create_publishable_entity_version( + entity.id, + version_num=1, + title="Entity v1", + created=self.now, + created_by=None, + ) + + with pytest.raises( + ValidationError, + match=f"Cannot publish learning package {lp1_id} while in bulk_draft_changes_for()." + ): + with publishing_api.bulk_draft_changes_for(lp1_id): + publishing_api.publish_all_drafts(lp1_id) + + with pytest.raises( + ValidationError, + match=f"Cannot publish learning package {lp1_id} while in bulk_draft_changes_for()." + ): + with publishing_api.bulk_draft_changes_for(lp1_id): + publishing_api.publish_from_drafts(lp1_id, Draft.objects.filter(entity__learning_package_id=lp1_id)) + + # But we CAN publish if the bulk_draft_changes_for is a different learning package: + with publishing_api.bulk_draft_changes_for(self.learning_package_2.id): + publishing_api.publish_all_drafts(lp1_id)