diff --git a/openedx/core/djangoapps/content_libraries/api/blocks.py b/openedx/core/djangoapps/content_libraries/api/blocks.py index 3678ea9f42d6..0400ede848de 100644 --- a/openedx/core/djangoapps/content_libraries/api/blocks.py +++ b/openedx/core/djangoapps/content_libraries/api/blocks.py @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/openedx/core/djangoapps/content_libraries/tests/test_course_to_library.py b/openedx/core/djangoapps/content_libraries/tests/test_course_to_library.py index 7bc7da0fc868..c63d7158eae5 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_course_to_library.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_course_to_library.py @@ -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