diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index b2b0cd1716..5c08340ecb 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -237,6 +237,11 @@ class TestSearchApi(ModuleStoreTestCase): title="Subsection 1", user_id=None, ) + library_api.update_container_children( + self.subsection.container_key, + [self.unit.container_key], + None, + ) self.subsection_key = "lct:org1:lib:subsection:subsection-1" self.section = library_api.create_container( self.library.key, @@ -246,6 +251,11 @@ class TestSearchApi(ModuleStoreTestCase): user_id=None, ) self.section_key = "lct:org1:lib:section:section-1" + library_api.update_container_children( + self.section.container_key, + [self.subsection.container_key], + None, + ) self.unit_dict = { "id": "lctorg1libunitunit-1-e4527f7c", @@ -278,10 +288,10 @@ class TestSearchApi(ModuleStoreTestCase): "type": "library_container", "display_name": "Subsection 1", # description is not set for containers - "num_children": 0, + "num_children": 1, "content": { - "child_usage_keys": [], - "child_display_names": [], + "child_usage_keys": ["lct:org1:lib:unit:unit-1"], + "child_display_names": ["Unit 1"], }, "publish_status": "never", "context_key": "lib:org1:lib", @@ -301,10 +311,10 @@ class TestSearchApi(ModuleStoreTestCase): "type": "library_container", "display_name": "Section 1", # description is not set for containers - "num_children": 0, + "num_children": 1, "content": { - "child_usage_keys": [], - "child_display_names": [], + "child_usage_keys": ["lct:org1:lib:subsection:subsection-1"], + "child_display_names": ["Subsection 1"], }, "publish_status": "never", "context_key": "lib:org1:lib", @@ -345,11 +355,11 @@ class TestSearchApi(ModuleStoreTestCase): doc_unit = copy.deepcopy(self.unit_dict) doc_unit["tags"] = {} doc_unit["collections"] = {'display_name': [], 'key': []} - doc_unit["subsections"] = {"display_name": [], "key": []} + doc_unit["subsections"] = {'display_name': ['Subsection 1'], 'key': ['lct:org1:lib:subsection:subsection-1']} doc_subsection = copy.deepcopy(self.subsection_dict) doc_subsection["tags"] = {} doc_subsection["collections"] = {'display_name': [], 'key': []} - doc_subsection["sections"] = {'display_name': [], 'key': []} + doc_subsection["sections"] = {'display_name': ['Section 1'], 'key': ['lct:org1:lib:section:section-1']} doc_section = copy.deepcopy(self.section_dict) doc_section["tags"] = {} doc_section["collections"] = {'display_name': [], 'key': []} @@ -387,11 +397,11 @@ class TestSearchApi(ModuleStoreTestCase): doc_unit = copy.deepcopy(self.unit_dict) doc_unit["tags"] = {} doc_unit["collections"] = {"display_name": [], "key": []} - doc_unit["subsections"] = {"display_name": [], "key": []} + doc_unit["subsections"] = {'display_name': ['Subsection 1'], 'key': ['lct:org1:lib:subsection:subsection-1']} doc_subsection = copy.deepcopy(self.subsection_dict) doc_subsection["tags"] = {} doc_subsection["collections"] = {'display_name': [], 'key': []} - doc_subsection["sections"] = {'display_name': [], 'key': []} + doc_subsection["sections"] = {'display_name': ['Section 1'], 'key': ['lct:org1:lib:section:section-1']} doc_section = copy.deepcopy(self.section_dict) doc_section["tags"] = {} doc_section["collections"] = {'display_name': [], 'key': []} @@ -989,12 +999,70 @@ class TestSearchApi(ModuleStoreTestCase): """ container = getattr(self, container_type) container_dict = getattr(self, f"{container_type}_dict") + update_doc_calls = [] + + def clear_contents(data: dict): + return { + **data, + "num_children": 0, + "content": { + "child_usage_keys": [], + "child_display_names": [], + }, + } + if container_type == "unit": + update_doc_calls.append(call([clear_contents(self.subsection_dict)])) + elif container_type == "subsection": + update_doc_calls.append(call([clear_contents(self.section_dict)])) + update_doc_calls.append(call([{ + 'id': self.unit_dict['id'], + 'subsections': {'display_name': [], 'key': []}, + }])) + elif container_type == "section": + update_doc_calls.append(call([{ + 'id': self.subsection_dict['id'], + 'sections': {'display_name': [], 'key': []}, + }])) library_api.delete_container(container.container_key) mock_meilisearch.return_value.index.return_value.delete_document.assert_called_once_with( container_dict["id"], ) + # Parent containers index data is updated. + if update_doc_calls: + mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( + update_doc_calls, + any_order=True, + ) + + # Restore + library_api.restore_container(container.container_key) + if container_type == "unit": + update_doc_calls.append(call([self.subsection_dict])) + elif container_type == "subsection": + update_doc_calls.append(call([self.section_dict])) + update_doc_calls.append(call([{ + 'id': self.unit_dict['id'], + 'subsections': { + 'display_name': [self.subsection_dict['display_name']], + 'key': [self.subsection_key], + }, + }])) + elif container_type == "section": + update_doc_calls.append(call([{ + 'id': self.subsection_dict['id'], + 'sections': { + 'display_name': [self.section_dict['display_name']], + 'key': [self.section_key], + }, + }])) + # Parent containers index data is updated on restore again. + if update_doc_calls: + mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( + update_doc_calls, + any_order=True, + ) @ddt.data( "unit", diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py index 1457586301..2385cc5b2e 100644 --- a/openedx/core/djangoapps/content_libraries/api/containers.py +++ b/openedx/core/djangoapps/content_libraries/api/containers.py @@ -308,6 +308,12 @@ def update_container( version: ContainerVersion affected_containers: list[ContainerMetadata] = [] + # Get children containers or components to update their index data + children = get_container_children( + container_key, + published=False, + ) + child_key_name = 'container_key' match container_type: case ContainerType.Unit: @@ -318,6 +324,8 @@ def update_container( created_by=user_id, ) affected_containers = get_containers_contains_item(container_key) + # Components have usage_key instead of container_key + child_key_name = 'usage_key' case ContainerType.Subsection: version = authoring_api.create_next_subsection_version( container.subsection, @@ -354,6 +362,16 @@ def update_container( container_key=affected_container.container_key, ) ) + # Update children components and containers index data, for example, + # All subsections under a section have section key in index that needs to be updated. + # So if parent section name has been changed, it needs to be reflected in sections key of children + for child in children: + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + content_object=ContentObjectChangedData( + object_id=str(getattr(child, child_key_name)), + changes=[container_key.container_type + "s"], + ), + ) return ContainerMetadata.from_container(library_key, version.container) @@ -369,11 +387,17 @@ def delete_container( library_key = container_key.lib_key container = _get_container_from_key(container_key) - affected_containers = get_containers_contains_item(container_key) + # Fetch related collections and containers before soft-delete affected_collections = authoring_api.get_entity_collections( container.publishable_entity.learning_package_id, container.key, ) + affected_containers = get_containers_contains_item(container_key) + # Get children containers or components to update their index data + children = get_container_children( + container_key, + published=False, + ) authoring_api.soft_delete_draft(container.pk) LIBRARY_CONTAINER_DELETED.send_event( @@ -404,6 +428,21 @@ def delete_container( container_key=affected_container.container_key, ) ) + container_type = ContainerType(container_key.container_type) + key_name = 'container_key' + if container_type == ContainerType.Unit: + # Components have usage_key instead of container_key + key_name = 'usage_key' + # Update children components and containers index data, for example, + # All subsections under a section have section key in index that needs to be updated. + # So if parent section is deleted, it needs to be removed from sections key of children + for child in children: + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + content_object=ContentObjectChangedData( + object_id=str(getattr(child, key_name)), + changes=[container_key.container_type + "s"], + ), + ) def restore_container(container_key: LibraryContainerLocator) -> None: @@ -419,6 +458,13 @@ def restore_container(container_key: LibraryContainerLocator) -> None: ) authoring_api.set_draft_version(container.pk, container.versioning.latest.pk) + # Fetch related containers after restore + affected_containers = get_containers_contains_item(container_key) + # Get children containers or components to update their index data + children = get_container_children( + container_key, + published=False, + ) LIBRARY_CONTAINER_CREATED.send_event( library_container=LibraryContainerData( @@ -426,11 +472,15 @@ def restore_container(container_key: LibraryContainerLocator) -> None: ) ) - # Add tags and collections back to index + content_changes = ["collections", "tags"] + if affected_containers and len(affected_containers) > 0: + # Update parent key data in index. Eg. `sections` key in index for subsection + content_changes.append(str(affected_containers[0].container_type.value) + "s") + # Add tags, collections and parent data back to index CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( content_object=ContentObjectChangedData( object_id=str(container_key), - changes=["collections", "tags"], + changes=content_changes, ), ) @@ -447,6 +497,28 @@ def restore_container(container_key: LibraryContainerLocator) -> None: ), ) ) + # Send events related to the containers that contains the updated container. + # This is to update the children display names used in the section/subsection previews. + for affected_container in affected_containers: + LIBRARY_CONTAINER_UPDATED.send_event( + library_container=LibraryContainerData( + container_key=affected_container.container_key, + ) + ) + container_type = ContainerType(container_key.container_type) + key_name = 'container_key' + if container_type == ContainerType.Unit: + key_name = 'usage_key' + # Update children components and containers index data, for example, + # All subsections under a section have section key in index that needs to be updated. + # Should restore removed parent section in sections key of children subsections + for child in children: + CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event( + content_object=ContentObjectChangedData( + object_id=str(getattr(child, key_name)), + changes=[container_key.container_type + "s"], + ), + ) def get_container_children(