diff --git a/cms/djangoapps/contentstore/tests/test_outlines.py b/cms/djangoapps/contentstore/tests/test_outlines.py index 3c0ceddfdfee..9f45fe0214ab 100644 --- a/cms/djangoapps/contentstore/tests/test_outlines.py +++ b/cms/djangoapps/contentstore/tests/test_outlines.py @@ -551,3 +551,79 @@ def test_task_invocation(self): outline = get_course_outline(course_key) assert len(outline.sections) == 1 assert len(outline.sequences) == 2 + + def test_bug_35535_regression_duplicate_section_appears_in_outline(self): + """ + Regression for https://github.com/openedx/edx-platform/issues/35535. + + Duplicating a Section (chapter) must leave the published branch in a + state where the generated outline includes the duplicated section AND + its sequences, without requiring any further Studio edit. + """ + from cms.djangoapps.contentstore.utils import duplicate_block + + with self.store.bulk_operations(self.course_key): + section = BlockFactory.create( + parent=self.draft_course, + category='chapter', + display_name="Original Section", + ) + for i in range(2): + BlockFactory.create( + parent=section, + category='sequential', + display_name=f"Original Seq {i}", + ) + + duplicate_block( + parent_usage_key=self.draft_course.location, + duplicate_source_usage_key=section.location, + user=self.user, + ) + + outline, _errs = get_outline_from_modulestore(self.course_key) + assert len(outline.sections) == 2 + # Each section must contain its two sequences (the original plus + # the fully-published duplicate subtree). + for outline_section in outline.sections: + assert len(outline_section.sequences) == 2 + + def test_bug_35535_regression_duplicate_subsection_appears_in_outline(self): + """ + Regression for #35535: duplicating a Subsection (sequential) must + make the duplicated sequence visible in the outline without any + follow-up publish. + """ + from cms.djangoapps.contentstore.utils import duplicate_block + + with self.store.bulk_operations(self.course_key): + section = BlockFactory.create( + parent=self.draft_course, + category='chapter', + display_name="Section For Subsection Duplicate", + ) + seq = BlockFactory.create( + parent=section, + category='sequential', + display_name="Original Seq", + ) + BlockFactory.create( + parent=seq, + category='vertical', + display_name="Unit", + ) + + duplicate_block( + parent_usage_key=section.location, + duplicate_source_usage_key=seq.location, + user=self.user, + ) + + outline, _errs = get_outline_from_modulestore(self.course_key) + duplicate_section = next( + (s for s in outline.sections + if s.title == "Section For Subsection Duplicate"), + None, + ) + assert duplicate_section is not None + assert len(duplicate_section.sequences) == 2 diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index b0b37a4a13b2..7cbc18530ecd 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -101,6 +101,7 @@ from xmodule.library_tools import LegacyLibraryToolsService from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.partitions.partitions_service import ( get_all_partitions_for_course, # lint-amnesty, pylint: disable=wrong-import-order @@ -1218,6 +1219,15 @@ def duplicate_block( parent.children.append(dest_block.location) store.update_item(parent, user.id) + # When a direct-only container (section/subsection) is duplicated at the top + # level, re-publish the full subtree so the published branch picks up the + # children that were built in the draft branch. The per-item auto-publish + # inside create_item/update_item uses EXCLUDE_ALL and leaves the published + # children list empty, which causes the generated course outline to omit + # the duplicated blocks until another publish happens. See issue #35535. + if not is_child and category in DIRECT_ONLY_CATEGORIES: + store.publish(dest_block.location, user.id) + # .. event_implemented_name: XBLOCK_DUPLICATED # .. event_type: org.openedx.content_authoring.xblock.duplicated.v1 XBLOCK_DUPLICATED.send_event(