Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions cms/djangoapps/contentstore/tests/test_outlines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down