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
12 changes: 11 additions & 1 deletion openedx/core/djangoapps/content_libraries/api/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,16 @@ def _import_staged_block(
staged_content_id: int,
staged_content_files: list[StagedContentFileData],
now: datetime,
*,
can_stand_alone: bool = True,
) -> LibraryXBlockMetadata:
"""
Create a new library block and populate it with staged content from clipboard

Set ``can_stand_alone=False`` when the block is being created as a child
of a container (e.g. a Unit being pasted from a course), so it is not
listed as a top-level library item. See bug #38132.

Returns the newly created library block
"""
from openedx.core.djangoapps.content_staging import api as content_staging_api
Expand Down Expand Up @@ -427,6 +433,7 @@ def _import_staged_block(
local_key=usage_key.block_id,
created=now,
created_by=user.id,
can_stand_alone=can_stand_alone,
)

# This will create the first component version and set the OLX/title
Expand Down Expand Up @@ -602,7 +609,9 @@ def _import_staged_block_as_container(
new_child_keys.append(child_container.container_key)
continue

# This is not a container, so we import it as a standalone block
# This is not a container, so we import it as a child component.
# Mark it as can_stand_alone=False so it is owned by the Unit and
# does not leak into the top-level library listing. See bug #38132.
try:
if copied_from_block in copied_from_map:
# This block was already copied from the library, so we just link it to the container
Expand All @@ -618,6 +627,7 @@ def _import_staged_block_as_container(
staged_content_id=staged_content_id,
staged_content_files=staged_content_files,
now=now,
can_stand_alone=False,
)
if copied_from_block:
copied_from_map[copied_from_block] = child_metadata.usage_key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,47 @@ def test_library_paste_unit_from_course(self):
"block_type": "poll_question",
})
assert children[3]["id"].startswith("lb:CL-TEST:test_lib_paste_clipboard:poll_question:change-your-answer-")

# Regression for bug #38132: every child must be marked
# can_stand_alone=False so it is owned by the Unit and does not
# leak into the top-level library listing. Before the fix the
# default was True and children appeared as orphan top-level items
# that could not be deleted cleanly.
for child in children:
assert child.get("can_stand_alone") is False, (
f"pasted child {child['id']} must have can_stand_alone=False"
)

def test_bug_38132_regression_pasted_unit_children_not_standalone(self):
"""
Regression test for bug #38132: after pasting a Unit from a course
into a library, the unit's children must be marked can_stand_alone=False
so the top-level library listing does not contain orphan components
that cannot be deleted.
"""
author = UserFactory.create(
username="Author38132",
email="author38132@example.com",
is_staff=True,
)
with self.as_user(author):
lib = self._create_library(
slug="test_lib_bug_38132",
title="Bug 38132 regression",
description="",
)
lib_id = lib["id"]

course_key = ToyCourseFactory.create().id
unit_key = course_key.make_usage_key("vertical", "vertical_test")
self._api(
'post',
"/api/content-staging/v1/clipboard/",
{"usage_key": str(unit_key)},
expect_response=200,
)
paste_data = self._paste_clipboard_content_in_library(lib_id)
children = self._get_container_children(paste_data["id"])
assert len(children) >= 1
for child in children:
assert child.get("can_stand_alone") is False