diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index d601abde8e6d..24f6a012478a 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -553,6 +553,22 @@ def upsert_library_block_index_doc(usage_key: UsageKey) -> None: _update_index_docs(docs) +def upsert_library_collection_index_doc(library_key: LibraryLocatorV2, collection_key: str) -> None: + """ + Creates or updates the document for the given Library Collection in the search index + """ + content_library = lib_api.ContentLibrary.objects.get_by_key(library_key) + collection = authoring_api.get_collection( + learning_package_id=content_library.learning_package_id, + collection_key=collection_key, + ) + docs = [ + searchable_doc_for_collection(collection) + ] + + _update_index_docs(docs) + + def upsert_content_library_index_docs(library_key: LibraryLocatorV2) -> None: """ Creates or updates the documents for the given Content Library in the search index diff --git a/openedx/core/djangoapps/content/search/handlers.py b/openedx/core/djangoapps/content/search/handlers.py index 0c2054ad39a8..47583baad091 100644 --- a/openedx/core/djangoapps/content/search/handlers.py +++ b/openedx/core/djangoapps/content/search/handlers.py @@ -6,20 +6,23 @@ from django.db.models.signals import post_delete from django.dispatch import receiver +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import UsageKey from openedx_events.content_authoring.data import ( ContentLibraryData, ContentObjectChangedData, LibraryBlockData, + LibraryCollectionData, XBlockData, ) -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import UsageKey from openedx_events.content_authoring.signals import ( CONTENT_LIBRARY_DELETED, CONTENT_LIBRARY_UPDATED, LIBRARY_BLOCK_CREATED, LIBRARY_BLOCK_DELETED, LIBRARY_BLOCK_UPDATED, + LIBRARY_COLLECTION_CREATED, + LIBRARY_COLLECTION_UPDATED, XBLOCK_CREATED, XBLOCK_DELETED, XBLOCK_UPDATED, @@ -34,6 +37,7 @@ delete_library_block_index_doc, delete_xblock_index_doc, update_content_library_index_docs, + update_library_collection_index_doc, upsert_library_block_index_doc, upsert_xblock_index_doc, ) @@ -151,6 +155,24 @@ def content_library_updated_handler(**kwargs) -> None: update_content_library_index_docs.apply(args=[str(content_library_data.library_key)]) +@receiver(LIBRARY_COLLECTION_CREATED) +@receiver(LIBRARY_COLLECTION_UPDATED) +@only_if_meilisearch_enabled +def library_collection_updated_handler(**kwargs) -> None: + """ + Create or update the index for the content library collection + """ + library_collection = kwargs.get("library_collection", None) + if not library_collection or not isinstance(library_collection, LibraryCollectionData): # pragma: no cover + log.error("Received null or incorrect data for event") + return + + update_library_collection_index_doc.delay( + str(library_collection.library_key), + library_collection.collection_key, + ) + + @receiver(CONTENT_OBJECT_ASSOCIATIONS_CHANGED) @only_if_meilisearch_enabled def content_object_associations_changed_handler(**kwargs) -> None: diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index dfd603776981..d9dad834db29 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -84,3 +84,16 @@ def update_content_library_index_docs(library_key_str: str) -> None: # Delete all documents in this library that were not published by above function # as this task is also triggered on discard event. api.delete_all_draft_docs_for_library(library_key) + + +@shared_task(base=LoggedTask, autoretry_for=(MeilisearchError, ConnectionError)) +@set_code_owner_attribute +def update_library_collection_index_doc(library_key_str: str, collection_key: str) -> None: + """ + Celery task to update the content index documents for a library collection + """ + library_key = LibraryLocatorV2.from_string(library_key_str) + + log.info("Updating content index documents for collection %s in library%s", collection_key, library_key) + + api.upsert_library_collection_index_doc(library_key, collection_key) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index f8f13d8b9a6f..d075681798bc 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -186,16 +186,16 @@ def setUp(self): description="my collection description" ) self.collection_dict = { - 'id': 'MYCOL', - 'type': 'collection', - 'display_name': 'my_collection', - 'description': 'my collection description', - 'context_key': 'lib:org1:lib', - 'org': 'org1', - 'created': created_date.timestamp(), - 'modified': created_date.timestamp(), + "id": "MYCOL", + "type": "collection", + "display_name": "my_collection", + "description": "my collection description", + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), "access_id": lib_access.id, - 'breadcrumbs': [{'display_name': 'Library'}] + "breadcrumbs": [{"display_name": "Library"}], } @override_settings(MEILISEARCH_ENABLED=False) @@ -409,38 +409,93 @@ def test_index_library_block_tags(self, mock_meilisearch): ) @override_settings(MEILISEARCH_ENABLED=True) - def test_index_library_block_collections(self, mock_meilisearch): + def test_index_library_block_and_collections(self, mock_meilisearch): """ - Test indexing an Library Block that is in two collections. + Test indexing an Library Block and the Collections it's in. """ - collection1 = authoring_api.create_collection( - learning_package_id=self.library.learning_package.id, - key="COL1", - title="Collection 1", - created_by=None, - description="First Collection", - ) - - collection2 = authoring_api.create_collection( - learning_package_id=self.library.learning_package.id, - key="COL2", - title="Collection 2", - created_by=None, - description="Second Collection", - ) + # Create collections (these internally call `upsert_library_collection_index_doc`) + created_date = datetime(2023, 5, 6, 7, 8, 9, tzinfo=timezone.utc) + with freeze_time(created_date): + collection1 = library_api.create_library_collection( + self.library.key, + collection_key="COL1", + title="Collection 1", + created_by=None, + description="First Collection", + ) - # Add Problem1 to both Collections (these internally call `upsert_block_collections_index_docs`) - # (adding in reverse order to test sorting of collection tag)) - for collection in (collection2, collection1): - library_api.update_library_collection_components( + collection2 = library_api.create_library_collection( self.library.key, - collection_key=collection.key, - usage_keys=[ - self.problem1.usage_key, - ], + collection_key="COL2", + title="Collection 2", + created_by=None, + description="Second Collection", ) - # Build expected docs with collections at each stage + # Add Problem1 to both Collections (these internally call `upsert_block_collections_index_docs` and + # `upsert_library_collection_index_doc`) + # (adding in reverse order to test sorting of collection tag) + updated_date = datetime(2023, 6, 7, 8, 9, 10, tzinfo=timezone.utc) + with freeze_time(updated_date): + for collection in (collection2, collection1): + library_api.update_library_collection_components( + self.library.key, + collection_key=collection.key, + usage_keys=[ + self.problem1.usage_key, + ], + ) + + # Build expected docs at each stage + lib_access, _ = SearchAccess.objects.get_or_create(context_key=self.library.key) + doc_collection1_created = { + "id": "COL1", + "type": "collection", + "display_name": "Collection 1", + "description": "First Collection", + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection2_created = { + "id": "COL2", + "type": "collection", + "display_name": "Collection 2", + "description": "Second Collection", + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": created_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection2_updated = { + "id": "COL2", + "type": "collection", + "display_name": "Collection 2", + "description": "Second Collection", + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": updated_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } + doc_collection1_updated = { + "id": "COL1", + "type": "collection", + "display_name": "Collection 1", + "description": "First Collection", + "context_key": "lib:org1:lib", + "org": "org1", + "created": created_date.timestamp(), + "modified": updated_date.timestamp(), + "access_id": lib_access.id, + "breadcrumbs": [{"display_name": "Library"}], + } doc_problem_with_collection2 = { "id": self.doc_problem1["id"], "collections": [collection2.key], @@ -452,6 +507,10 @@ def test_index_library_block_collections(self, mock_meilisearch): mock_meilisearch.return_value.index.return_value.update_documents.assert_has_calls( [ + call([doc_collection1_created]), + call([doc_collection2_created]), + call([doc_collection2_updated]), + call([doc_collection1_updated]), call([doc_problem_with_collection2]), call([doc_problem_with_collection1]), ],