Skip to content

fix(contentstore): publish duplicated direct-only subtrees in full#38317

Open
kingoftech-v01 wants to merge 1 commit intoopenedx:masterfrom
kingoftech-v01:fix/bug-35535
Open

fix(contentstore): publish duplicated direct-only subtrees in full#38317
kingoftech-v01 wants to merge 1 commit intoopenedx:masterfrom
kingoftech-v01:fix/bug-35535

Conversation

@kingoftech-v01
Copy link
Copy Markdown

Description

Duplicating a Section or Subsection in Studio leaves the newly duplicated blocks silently missing from the generated course outline — they only appear after an unrelated edit forces another publish.

User impact (Course Author): Duplicated sections/subsections appear in the learner-facing outline immediately, with their full child tree.

Root Cause

In cms/djangoapps/contentstore/utils.py:duplicate_block():

  1. store.create_item(...) triggers split modulestore's _auto_publish_no_children, which calls publish(..., blacklist=EXCLUDE_ALL). The published block is created with children=[].
  2. Recursive child duplication + store.update_item(dest_block, ...) triggers another _auto_publish_no_children with EXCLUDE_ALL. _copy_subdag guards at line 3133 skip copying children to the published branch.
  3. The published branch of the new section ends with an empty children list.
  4. On bulk_operations exit, course_published fires → update_outline_from_modulestore_task reads the published branch → sees an empty duplicated section → silently drops it from the outline.
  5. Any later manual Publish from Studio calls publish() without EXCLUDE_ALL, which finally copies the subtree to the published branch.

EXCLUDE_ALL was added by Mathew Peterson on 2014-07-21 (commit 9a039e93ec, LMS-11017) for an important reason: it prevents a container save from cascading into an unwanted publish of separately-edited unpublished unit drafts. We must NOT remove it from _auto_publish_no_children.

Fix

Scope the fix to the call site. After duplicate_block() has assembled the full subtree in the draft branch, issue one explicit store.publish(dest_block.location, user.id) (no blacklist) only when:

  1. The duplicated block is in DIRECT_ONLY_CATEGORIES (sections, subsections — not verticals/components), AND
  2. We are at the top of the recursion (is_child=False).

This leaves leaf duplication (verticals, components) draft-only, preserving the 2014 protection against stomping unpublished component drafts. The new publish happens inside the same bulk_operations context, so course_published still fires exactly once.

Supporting Information

  • Issue: Duplicated Blocks are missing from the Generated Course Outline #35535
  • Original EXCLUDE_ALL intent: commit 9a039e93ec (LMS-11017, 2014-07-21, Mathew Peterson) and abbfa95e4c (LMS-11168, Nimisha Asthagiri) — designed to protect unpublished unit content from cascading publishes.
  • DIRECT_ONLY_CATEGORIES = ['course', 'chapter', 'sequential', 'about', 'static_tab', 'course_info'] — only containers that are always-published by design.

Testing Instructions

  1. Create a course with at least one section containing sequences and units.
  2. Duplicate a Section in Studio.
  3. Reload the course outline — the duplicated section must appear with all its sequences (before the fix, it would be missing or empty).
  4. Duplicate a Subsection — the duplicate must appear in the outline immediately.
  5. Verify duplicating a Vertical still only affects the draft branch (not published) — unpublished component drafts must remain untouched.

Two new regression tests in cms/djangoapps/contentstore/tests/test_outlines.py:

  • test_bug_35535_regression_duplicate_section_appears_in_outline
  • test_bug_35535_regression_duplicate_subsection_appears_in_outline

Deadline

None

Other Information

  • No database migrations
  • No new dependencies
  • Single course_published signal still fires (new publish is inside the existing bulk_operations context)
  • Leaf block duplication (verticals, components) remains draft-only — no risk of stomping unpublished component drafts
  • copy_from_template in split_draft.py may have a similar latent issue but is out of scope for this fix

Closes #35535

When duplicating a Section or Subsection in Studio, the modulestore's
per-item auto-publish uses blacklist=EXCLUDE_ALL, leaving the published
branch with an empty children list for the newly duplicated container.
The course_published signal then regenerates the outline from the
published branch and silently drops the duplicated blocks until an
unrelated edit triggers another publish.

After duplicate_block has built the full subtree in the draft branch,
issue a proper store.publish() (no blacklist) for the top-level
duplicated block when it is in DIRECT_ONLY_CATEGORIES, so the published
branch matches the draft before course_published fires. Leaf block
duplications (verticals, components) are unaffected and remain
draft-only, preserving unpublished unit drafts.

Closes openedx#35535
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Apr 10, 2026
@openedx-webhooks
Copy link
Copy Markdown

Thanks for the pull request, @kingoftech-v01!

This repository is currently maintained by @openedx/wg-maintenance-openedx-platform.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Submit a signed contributor agreement (CLA)

⚠️ We ask all contributors to the Open edX project to submit a signed contributor agreement or indicate their institutional affiliation.
Please see the CONTRIBUTING file for more information.

If you've signed an agreement in the past, you may need to re-sign.
See The New Home of the Open edX Codebase for details.

Once you've signed the CLA, please allow 1 business day for it to be processed.
After this time, you can re-run the CLA check by adding a comment below that you have signed it.
If the CLA check continues to fail, you can tag the @openedx/cla-problems team in a comment for further assistance.

🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Needs Triage

Development

Successfully merging this pull request may close these issues.

Duplicated Blocks are missing from the Generated Course Outline

2 participants