From b8b10a17cd61207ee11abf73852b00e5dbdc5347 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 1 Apr 2025 21:15:11 -0500 Subject: [PATCH 1/8] fix: Delete index when delete container --- openedx/core/djangoapps/content/search/api.py | 6 +++--- .../djangoapps/content/search/documents.py | 6 +++--- .../djangoapps/content/search/handlers.py | 18 ++++++++++++++++- .../core/djangoapps/content/search/tasks.py | 20 ++++++++++++++++++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index a597ae51e865..be20b2b76350 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -17,7 +17,7 @@ from meilisearch import Client as MeilisearchClient from meilisearch.errors import MeilisearchApiError, MeilisearchError from meilisearch.models.task import TaskInfo -from opaque_keys.edx.keys import UsageKey +from opaque_keys.edx.keys import UsageKey, OpaqueKey from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2, LibraryCollectionLocator from openedx_learning.api import authoring as authoring_api from common.djangoapps.student.roles import GlobalStaff @@ -613,12 +613,12 @@ def add_with_children(block): _update_index_docs(docs) -def delete_index_doc(usage_key: UsageKey) -> None: +def delete_index_doc(usage_key: OpaqueKey) -> None: """ Deletes the document for the given XBlock from the search index Args: - usage_key (UsageKey): The usage key of the XBlock to be removed from the index + usage_key (OpaqueKey): The usage key of the XBlock to be removed from the index """ doc = searchable_doc_for_usage_key(usage_key) _delete_index_doc(doc[Fields.id]) diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py index c6ca52098abc..bce894bb3f63 100644 --- a/openedx/core/djangoapps/content/search/documents.py +++ b/openedx/core/djangoapps/content/search/documents.py @@ -8,7 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.text import slugify -from opaque_keys.edx.keys import LearningContextKey, UsageKey +from opaque_keys.edx.keys import LearningContextKey, UsageKey, OpaqueKey from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2 from openedx_learning.api import authoring as authoring_api from openedx_learning.api.authoring_models import Collection @@ -113,7 +113,7 @@ class PublishStatus: modified = "modified" -def meili_id_from_opaque_key(usage_key: UsageKey) -> str: +def meili_id_from_opaque_key(usage_key: OpaqueKey) -> str: """ Meilisearch requires each document to have a primary key that's either an integer or a string composed of alphanumeric characters (a-z A-Z 0-9), @@ -140,7 +140,7 @@ def _meili_access_id_from_context_key(context_key: LearningContextKey) -> int: return access.id -def searchable_doc_for_usage_key(usage_key: UsageKey) -> dict: +def searchable_doc_for_usage_key(usage_key: OpaqueKey) -> dict: """ Generates a base document identified by its usage key. """ diff --git a/openedx/core/djangoapps/content/search/handlers.py b/openedx/core/djangoapps/content/search/handlers.py index 4565165e87ea..91aada04eded 100644 --- a/openedx/core/djangoapps/content/search/handlers.py +++ b/openedx/core/djangoapps/content/search/handlers.py @@ -46,6 +46,7 @@ ) from .tasks import ( delete_library_block_index_doc, + delete_library_container_index_doc, delete_xblock_index_doc, update_content_library_index_docs, update_library_collection_index_doc, @@ -233,7 +234,6 @@ def content_object_associations_changed_handler(**kwargs) -> None: @receiver(LIBRARY_CONTAINER_CREATED) -@receiver(LIBRARY_CONTAINER_DELETED) @receiver(LIBRARY_CONTAINER_UPDATED) @only_if_meilisearch_enabled def library_container_updated_handler(**kwargs) -> None: @@ -258,3 +258,19 @@ def library_container_updated_handler(**kwargs) -> None: str(library_container.library_key), library_container.container_key, ]) + + +@receiver(LIBRARY_CONTAINER_DELETED) +@only_if_meilisearch_enabled +def library_container_deleted(**kwargs) -> None: + """ + Delete the index for the content library container + """ + library_container = kwargs.get("library_container", None) + if not library_container or not isinstance(library_container, LibraryContainerData): # pragma: no cover + log.error("Received null or incorrect data for event") + return + + # Update content library index synchronously to make sure that search index is updated before + # the frontend invalidates/refetches results. This is only a single document update so is very fast. + delete_library_container_index_doc.apply(args=[str(library_container.container_key)]) diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index f23ca9aa304e..29a540e6b357 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -11,7 +11,12 @@ from edx_django_utils.monitoring import set_code_owner_attribute from meilisearch.errors import MeilisearchError from opaque_keys.edx.keys import UsageKey -from opaque_keys.edx.locator import LibraryContainerLocator, LibraryLocatorV2, LibraryUsageLocatorV2 +from opaque_keys.edx.locator import ( + LibraryContainerLocator, + LibraryLocatorV2, + LibraryUsageLocatorV2, + LibraryContainerLocator, +) from . import api @@ -124,3 +129,16 @@ def update_library_container_index_doc(library_key_str: str, container_key_str: log.info("Updating content index documents for container %s in library%s", container_key, library_key) api.upsert_library_container_index_doc(container_key) + + +@shared_task(base=LoggedTask, autoretry_for=(MeilisearchError, ConnectionError)) +@set_code_owner_attribute +def delete_library_container_index_doc(container_key_str: str) -> None: + """ + Celery task to delete the content index document for a library block + """ + container_key = LibraryContainerLocator.from_string(container_key_str) + + log.info("Deleting content index document for library block with id: %s", container_key) + + api.delete_index_doc(container_key) \ No newline at end of file From 1515e7e4383a57f9abeef6fb01ff281df2acb513 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 2 Apr 2025 18:25:26 -0500 Subject: [PATCH 2/8] test: Test for delete container index --- openedx/core/djangoapps/content/search/tasks.py | 3 +-- .../core/djangoapps/content/search/tests/test_api.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/content/search/tasks.py b/openedx/core/djangoapps/content/search/tasks.py index 29a540e6b357..1c66050415e3 100644 --- a/openedx/core/djangoapps/content/search/tasks.py +++ b/openedx/core/djangoapps/content/search/tasks.py @@ -15,7 +15,6 @@ LibraryContainerLocator, LibraryLocatorV2, LibraryUsageLocatorV2, - LibraryContainerLocator, ) from . import api @@ -141,4 +140,4 @@ def delete_library_container_index_doc(container_key_str: str) -> None: log.info("Deleting content index document for library block with id: %s", container_key) - api.delete_index_doc(container_key) \ No newline at end of file + api.delete_index_doc(container_key) diff --git a/openedx/core/djangoapps/content/search/tests/test_api.py b/openedx/core/djangoapps/content/search/tests/test_api.py index 813db0241db1..8b9f8f710d0d 100644 --- a/openedx/core/djangoapps/content/search/tests/test_api.py +++ b/openedx/core/djangoapps/content/search/tests/test_api.py @@ -866,3 +866,14 @@ def test_delete_collection(self, mock_meilisearch): mock_meilisearch.return_value.index.return_value.update_documents.assert_called_once_with([ doc_problem_without_collection, ]) + + @override_settings(MEILISEARCH_ENABLED=True) + def test_delete_index_container(self, mock_meilisearch): + """ + Test delete a container index. + """ + library_api.delete_container(self.unit.container_key) + + mock_meilisearch.return_value.index.return_value.delete_document.assert_called_once_with( + self.unit_dict["id"], + ) From ffad068a88b40541ad421a63f12a7b344de6b010 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 2 Apr 2025 18:34:04 -0500 Subject: [PATCH 3/8] refactor: Rename searchable_doc_for_key --- openedx/core/djangoapps/content/search/api.py | 8 +++---- .../djangoapps/content/search/documents.py | 22 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openedx/core/djangoapps/content/search/api.py b/openedx/core/djangoapps/content/search/api.py index be20b2b76350..ea0877489335 100644 --- a/openedx/core/djangoapps/content/search/api.py +++ b/openedx/core/djangoapps/content/search/api.py @@ -42,7 +42,7 @@ searchable_doc_for_collection, searchable_doc_for_container, searchable_doc_for_library_block, - searchable_doc_for_usage_key, + searchable_doc_for_key, searchable_doc_collections, searchable_doc_tags, searchable_doc_tags_for_collection, @@ -613,14 +613,14 @@ def add_with_children(block): _update_index_docs(docs) -def delete_index_doc(usage_key: OpaqueKey) -> None: +def delete_index_doc(key: OpaqueKey) -> None: """ Deletes the document for the given XBlock from the search index Args: - usage_key (OpaqueKey): The usage key of the XBlock to be removed from the index + key (OpaqueKey): The opaque key of the XBlock/Container to be removed from the index """ - doc = searchable_doc_for_usage_key(usage_key) + doc = searchable_doc_for_key(key) _delete_index_doc(doc[Fields.id]) diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py index bce894bb3f63..aed4eac597b5 100644 --- a/openedx/core/djangoapps/content/search/documents.py +++ b/openedx/core/djangoapps/content/search/documents.py @@ -113,7 +113,7 @@ class PublishStatus: modified = "modified" -def meili_id_from_opaque_key(usage_key: OpaqueKey) -> str: +def meili_id_from_opaque_key(key: OpaqueKey) -> str: """ Meilisearch requires each document to have a primary key that's either an integer or a string composed of alphanumeric characters (a-z A-Z 0-9), @@ -124,7 +124,7 @@ def meili_id_from_opaque_key(usage_key: OpaqueKey) -> str: we could use PublishableEntity's primary key / UUID instead. """ # The slugified key _may_ not be unique so we append a hashed string to make it unique: - key_str = str(usage_key) + key_str = str(key) key_bin = key_str.encode() suffix = blake2b(key_bin, digest_size=4, usedforsecurity=False).hexdigest() @@ -140,12 +140,12 @@ def _meili_access_id_from_context_key(context_key: LearningContextKey) -> int: return access.id -def searchable_doc_for_usage_key(usage_key: OpaqueKey) -> dict: +def searchable_doc_for_key(key: OpaqueKey) -> dict: """ - Generates a base document identified by its usage key. + Generates a base document identified by its opaque key. """ return { - Fields.id: meili_id_from_opaque_key(usage_key), + Fields.id: meili_id_from_opaque_key(key), } @@ -406,7 +406,7 @@ def searchable_doc_for_library_block(xblock_metadata: lib_api.LibraryXBlockMetad block_published = None publish_status = PublishStatus.never - doc = searchable_doc_for_usage_key(xblock_metadata.usage_key) + doc = searchable_doc_for_key(xblock_metadata.usage_key) doc.update({ Fields.type: DocType.library_block, Fields.breadcrumbs: [], @@ -432,7 +432,7 @@ def searchable_doc_tags(usage_key: UsageKey) -> dict: Generate a dictionary document suitable for ingestion into a search engine like Meilisearch or Elasticsearch, with the tags data for the given content object. """ - doc = searchable_doc_for_usage_key(usage_key) + doc = searchable_doc_for_key(usage_key) doc.update(_tags_for_content_object(usage_key)) return doc @@ -443,7 +443,7 @@ def searchable_doc_collections(usage_key: UsageKey) -> dict: Generate a dictionary document suitable for ingestion into a search engine like Meilisearch or Elasticsearch, with the collections data for the given content object. """ - doc = searchable_doc_for_usage_key(usage_key) + doc = searchable_doc_for_key(usage_key) doc.update(_collections_for_content_object(usage_key)) return doc @@ -461,7 +461,7 @@ def searchable_doc_tags_for_collection( library_key, collection_key, ) - doc = searchable_doc_for_usage_key(collection_usage_key) + doc = searchable_doc_for_key(collection_usage_key) doc.update(_tags_for_content_object(collection_usage_key)) return doc @@ -473,7 +473,7 @@ def searchable_doc_for_course_block(block) -> dict: like Meilisearch or Elasticsearch, so that the given course block can be found using faceted search. """ - doc = searchable_doc_for_usage_key(block.usage_key) + doc = searchable_doc_for_key(block.usage_key) doc.update({ Fields.type: DocType.course_block, }) @@ -503,7 +503,7 @@ def searchable_doc_for_collection( collection_key, ) - doc = searchable_doc_for_usage_key(collection_usage_key) + doc = searchable_doc_for_key(collection_usage_key) try: collection = collection or lib_api.get_library_collection_from_usage_key(collection_usage_key) From 7b63e81dd34dc76886363e863e2e9b530b1e7848 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 7 Apr 2025 19:07:25 -0500 Subject: [PATCH 4/8] feat: Restore container api & rest_api added --- .../content_libraries/api/containers.py | 24 +++++++- .../content_libraries/rest_api/containers.py | 20 ++++++ .../content_libraries/tests/base.py | 5 ++ .../tests/test_containers.py | 61 +++++++++++++++++++ .../core/djangoapps/content_libraries/urls.py | 2 + 5 files changed, 110 insertions(+), 2 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py index bf519901ae57..a15fa944eaa8 100644 --- a/openedx/core/djangoapps/content_libraries/api/containers.py +++ b/openedx/core/djangoapps/content_libraries/api/containers.py @@ -44,6 +44,7 @@ "library_container_locator", "update_container", "delete_container", + "restore_container", "update_container_children", "get_containers_contains_component", ] @@ -121,7 +122,7 @@ def library_container_locator( ) -def _get_container(container_key: LibraryContainerLocator) -> Container: +def _get_container(container_key: LibraryContainerLocator, isDeleted=False) -> Container: """ Internal method to fetch the Container object from its LibraryContainerLocator @@ -135,7 +136,7 @@ def _get_container(container_key: LibraryContainerLocator) -> Container: learning_package.id, key=container_key.container_id, ) - if container and container.versioning.draft: + if container and (isDeleted or container.versioning.draft): return container raise ContentLibraryContainerNotFound @@ -251,6 +252,25 @@ def delete_container( # TODO: trigger a LIBRARY_COLLECTION_UPDATED for each collection the container was in +def restore_container(container_key: LibraryContainerLocator) -> None: + """ + Restore the specified library container. + """ + try: + container = _get_container(container_key, isDeleted=True) + except ContentLibraryContainerNotFound: + return + + authoring_api.set_draft_version(container.pk, container.versioning.latest.pk) + + LIBRARY_CONTAINER_CREATED.send_event( + library_container=LibraryContainerData( + library_key=container_key.library_key, + container_key=str(container_key), + ) + ) + + def get_container_children( container_key: LibraryContainerLocator, published=False, diff --git a/openedx/core/djangoapps/content_libraries/rest_api/containers.py b/openedx/core/djangoapps/content_libraries/rest_api/containers.py index 95e468b4a43a..3c65fc006bf6 100644 --- a/openedx/core/djangoapps/content_libraries/rest_api/containers.py +++ b/openedx/core/djangoapps/content_libraries/rest_api/containers.py @@ -273,3 +273,23 @@ def patch(self, request, container_key: LibraryContainerLocator): container_key, action=authoring_api.ChildrenEntitiesAction.REPLACE, ) + + +@method_decorator(non_atomic_requests, name="dispatch") +@view_auth_classes() +class LibraryContainerRestore(GenericAPIView): + """ + View to restore soft-deleted library containers. + """ + @convert_exceptions + def post(self, request, container_key: LibraryContainerLocator) -> Response: + """ + Restores a soft-deleted library container + """ + api.require_permission_for_library_key( + container_key.library_key, + request.user, + permissions.CAN_EDIT_THIS_CONTENT_LIBRARY, + ) + api.restore_container(container_key) + return Response(None, status=HTTP_204_NO_CONTENT) diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py index 6adb8184ea00..3c07aabe06c4 100644 --- a/openedx/core/djangoapps/content_libraries/tests/base.py +++ b/openedx/core/djangoapps/content_libraries/tests/base.py @@ -34,6 +34,7 @@ URL_LIB_BLOCK_ASSET_FILE = URL_LIB_BLOCK + 'assets/{file_name}' # Get, delete, or upload a specific static asset file URL_LIB_CONTAINER = URL_PREFIX + 'containers/{container_key}/' # Get a container in this library URL_LIB_CONTAINER_COMPONENTS = URL_LIB_CONTAINER + 'children/' # Get, add or delete a component in this container +URL_LIB_CONTAINER_RESTORE = URL_LIB_CONTAINER + 'restore/' # Restore a deleted container URL_LIB_LTI_PREFIX = URL_PREFIX + 'lti/1.3/' URL_LIB_LTI_JWKS = URL_LIB_LTI_PREFIX + 'pub/jwks/' @@ -386,6 +387,10 @@ def _delete_container(self, container_key: str, expect_response=204): """ Delete a container (unit etc.) """ return self._api('delete', URL_LIB_CONTAINER.format(container_key=container_key), None, expect_response) + def _restore_container(self, container_key: str, expect_response=204): + """ Restore a deleted a container (unit etc.) """ + return self._api('post', URL_LIB_CONTAINER_RESTORE.format(container_key=container_key), None, expect_response) + def _get_container_components(self, container_key: str, expect_response=200): """ Get container components""" return self._api( diff --git a/openedx/core/djangoapps/content_libraries/tests/test_containers.py b/openedx/core/djangoapps/content_libraries/tests/test_containers.py index 52546396f2bb..798776d6422f 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_containers.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_containers.py @@ -330,3 +330,64 @@ def test_unit_replace_children(self): }, update_receiver.call_args_list[0].kwargs, ) + + def test_restore_unit(self): + """ + Test restore a deleted unit. + """ + lib = self._create_library(slug="containers", title="Container Test Library", description="Units and more") + lib_key = LibraryLocatorV2.from_string(lib["id"]) + + create_receiver = mock.Mock() + LIBRARY_CONTAINER_CREATED.connect(create_receiver) + + # Create a unit: + create_date = datetime(2024, 9, 8, 7, 6, 5, tzinfo=timezone.utc) + with freeze_time(create_date): + container_data = self._create_container(lib["id"], "unit", slug="u1", display_name="Test Unit") + + # Delete the unit + self._delete_container(container_data["container_key"]) + + # Restore container + self._restore_container(container_data["container_key"]) + new_container_data = self._get_container(container_data["container_key"]) + expected_data = { + "container_key": "lct:CL-TEST:containers:unit:u1", + "container_type": "unit", + "display_name": "Test Unit", + "last_published": None, + "published_by": "", + "last_draft_created": "2024-09-08T07:06:05Z", + "last_draft_created_by": 'Bob', + 'has_unpublished_changes': True, + 'created': '2024-09-08T07:06:05Z', + 'modified': '2024-09-08T07:06:05Z', + 'collections': [], + } + + self.assertDictContainsEntries(new_container_data, expected_data) + + assert create_receiver.call_count == 2 + self.assertDictContainsSubset( + { + "signal": LIBRARY_CONTAINER_CREATED, + "sender": None, + "library_container": LibraryContainerData( + lib_key, + container_key="lct:CL-TEST:containers:unit:u1", + ), + }, + create_receiver.call_args_list[0].kwargs, + ) + self.assertDictContainsSubset( + { + "signal": LIBRARY_CONTAINER_CREATED, + "sender": None, + "library_container": LibraryContainerData( + lib_key, + container_key="lct:CL-TEST:containers:unit:u1", + ), + }, + create_receiver.call_args_list[1].kwargs, + ) diff --git a/openedx/core/djangoapps/content_libraries/urls.py b/openedx/core/djangoapps/content_libraries/urls.py index 99e98a2cc424..474d959d1eb4 100644 --- a/openedx/core/djangoapps/content_libraries/urls.py +++ b/openedx/core/djangoapps/content_libraries/urls.py @@ -82,6 +82,8 @@ path('', containers.LibraryContainerView.as_view()), # update components under container path('children/', containers.LibraryContainerChildrenView.as_view()), + # Restore the container + path('restore/', containers.LibraryContainerRestore.as_view()), # Update collections for a given container # path('collections/', views.LibraryContainerCollectionsView.as_view(), name='update-collections-ct'), # path('publish/', views.LibraryContainerPublishView.as_view()), From a05e089d5ec8ab6463c82f468dc946f51404d95d Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 7 Apr 2025 19:15:29 -0500 Subject: [PATCH 5/8] fix: Broken lint --- .../core/djangoapps/content_libraries/tests/test_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_containers.py b/openedx/core/djangoapps/content_libraries/tests/test_containers.py index 798776d6422f..655fd11c40c8 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_containers.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_containers.py @@ -348,7 +348,7 @@ def test_restore_unit(self): # Delete the unit self._delete_container(container_data["container_key"]) - + # Restore container self._restore_container(container_data["container_key"]) new_container_data = self._get_container(container_data["container_key"]) From d08fd3b30e9fd4542d474502197311ba99aa1737 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Mon, 7 Apr 2025 19:22:58 -0500 Subject: [PATCH 6/8] fix: Broken lint --- .../core/djangoapps/content_libraries/tests/test_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_containers.py b/openedx/core/djangoapps/content_libraries/tests/test_containers.py index 655fd11c40c8..8fb36d3fd12c 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_containers.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_containers.py @@ -348,7 +348,7 @@ def test_restore_unit(self): # Delete the unit self._delete_container(container_data["container_key"]) - + # Restore container self._restore_container(container_data["container_key"]) new_container_data = self._get_container(container_data["container_key"]) From 4344c19c72f117ec671eb09843bbafa25e3c1744 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 8 Apr 2025 17:58:49 -0500 Subject: [PATCH 7/8] style: Nits on the code --- .../tests/test_containers.py | 19 ++++--------------- .../core/djangoapps/content_libraries/urls.py | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/tests/test_containers.py b/openedx/core/djangoapps/content_libraries/tests/test_containers.py index 8fb36d3fd12c..46d206af5008 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_containers.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_containers.py @@ -338,9 +338,6 @@ def test_restore_unit(self): lib = self._create_library(slug="containers", title="Container Test Library", description="Units and more") lib_key = LibraryLocatorV2.from_string(lib["id"]) - create_receiver = mock.Mock() - LIBRARY_CONTAINER_CREATED.connect(create_receiver) - # Create a unit: create_date = datetime(2024, 9, 8, 7, 6, 5, tzinfo=timezone.utc) with freeze_time(create_date): @@ -349,6 +346,9 @@ def test_restore_unit(self): # Delete the unit self._delete_container(container_data["container_key"]) + create_receiver = mock.Mock() + LIBRARY_CONTAINER_CREATED.connect(create_receiver) + # Restore container self._restore_container(container_data["container_key"]) new_container_data = self._get_container(container_data["container_key"]) @@ -368,7 +368,7 @@ def test_restore_unit(self): self.assertDictContainsEntries(new_container_data, expected_data) - assert create_receiver.call_count == 2 + assert create_receiver.call_count == 1 self.assertDictContainsSubset( { "signal": LIBRARY_CONTAINER_CREATED, @@ -380,14 +380,3 @@ def test_restore_unit(self): }, create_receiver.call_args_list[0].kwargs, ) - self.assertDictContainsSubset( - { - "signal": LIBRARY_CONTAINER_CREATED, - "sender": None, - "library_container": LibraryContainerData( - lib_key, - container_key="lct:CL-TEST:containers:unit:u1", - ), - }, - create_receiver.call_args_list[1].kwargs, - ) diff --git a/openedx/core/djangoapps/content_libraries/urls.py b/openedx/core/djangoapps/content_libraries/urls.py index 474d959d1eb4..c46cc8641cd8 100644 --- a/openedx/core/djangoapps/content_libraries/urls.py +++ b/openedx/core/djangoapps/content_libraries/urls.py @@ -82,7 +82,7 @@ path('', containers.LibraryContainerView.as_view()), # update components under container path('children/', containers.LibraryContainerChildrenView.as_view()), - # Restore the container + # Restore a soft-deleted container path('restore/', containers.LibraryContainerRestore.as_view()), # Update collections for a given container # path('collections/', views.LibraryContainerCollectionsView.as_view(), name='update-collections-ct'), From 06960ff5336d2400762eef76def4e1149c3e8f5a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Tue, 8 Apr 2025 18:15:29 -0500 Subject: [PATCH 8/8] refactor: delete_container & restore_container API --- .../djangoapps/content_libraries/api/containers.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py index a15fa944eaa8..8a80cec352aa 100644 --- a/openedx/core/djangoapps/content_libraries/api/containers.py +++ b/openedx/core/djangoapps/content_libraries/api/containers.py @@ -235,10 +235,7 @@ def delete_container( No-op if container doesn't exist or has already been soft-deleted. """ - try: - container = _get_container(container_key) - except ContentLibraryContainerNotFound: - return + container = _get_container(container_key) authoring_api.soft_delete_draft(container.pk) @@ -256,10 +253,7 @@ def restore_container(container_key: LibraryContainerLocator) -> None: """ Restore the specified library container. """ - try: - container = _get_container(container_key, isDeleted=True) - except ContentLibraryContainerNotFound: - return + container = _get_container(container_key, isDeleted=True) authoring_api.set_draft_version(container.pk, container.versioning.latest.pk)