Skip to content

Commit

Permalink
feat: api to restore soft-deleted component [FC-0076] (#35993)
Browse files Browse the repository at this point in the history
Adds API to handle restoring soft-deleted library blocks.
  • Loading branch information
navinkarkera authored Dec 13, 2024
1 parent f4d110c commit 971afe6
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 24 deletions.
5 changes: 4 additions & 1 deletion openedx/core/djangoapps/content/search/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,10 @@ def _collections_for_content_object(object_id: UsageKey | LearningContextKey) ->
If the object is in no collections, returns:
{
"collections": {},
"collections": {
"display_name": [],
"key": [],
},
}
"""
Expand Down
10 changes: 10 additions & 0 deletions openedx/core/djangoapps/content/search/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,13 @@ def test_create_delete_library_block(self, meilisearch_client):
meilisearch_client.return_value.index.return_value.delete_document.assert_called_with(
"lborgalib_aproblemproblem1-ca3186e9"
)

# Restore the Library Block
library_api.restore_library_block(problem.usage_key)
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call([doc_problem])
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call(
[{'id': doc_problem['id'], 'collections': {'display_name': [], 'key': []}}]
)
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call(
[{'id': doc_problem['id'], 'tags': {}}]
)
40 changes: 40 additions & 0 deletions openedx/core/djangoapps/content_libraries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,46 @@ def delete_library_block(usage_key, remove_from_parent=True):
)


def restore_library_block(usage_key):
"""
Restore the specified library block.
"""
component = get_component_from_usage_key(usage_key)
library_key = usage_key.context_key
affected_collections = authoring_api.get_entity_collections(component.learning_package_id, component.key)

# Set draft version back to the latest available component version id.
authoring_api.set_draft_version(component.pk, component.versioning.latest.pk)

LIBRARY_BLOCK_CREATED.send_event(
library_block=LibraryBlockData(
library_key=library_key,
usage_key=usage_key
)
)

# Add tags and collections back to index
CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event(
content_object=ContentObjectChangedData(
object_id=str(usage_key),
changes=["collections", "tags"],
),
)

# For each collection, trigger LIBRARY_COLLECTION_UPDATED signal and set background=True to trigger
# collection indexing asynchronously.
#
# To restore the component in the collections
for collection in affected_collections:
LIBRARY_COLLECTION_UPDATED.send_event(
library_collection=LibraryCollectionData(
library_key=library_key,
collection_key=collection.key,
background=True,
)
)


def get_library_block_static_asset_files(usage_key) -> list[LibraryXBlockStaticFile]:
"""
Given an XBlock in a content library, list all the static asset files
Expand Down
29 changes: 29 additions & 0 deletions openedx/core/djangoapps/content_libraries/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,35 @@ def test_delete_library_block(self):
event_receiver.call_args_list[0].kwargs,
)

def test_restore_library_block(self):
api.update_library_collection_components(
self.lib1.library_key,
self.col1.key,
usage_keys=[
UsageKey.from_string(self.lib1_problem_block["id"]),
UsageKey.from_string(self.lib1_html_block["id"]),
],
)

event_receiver = mock.Mock()
LIBRARY_COLLECTION_UPDATED.connect(event_receiver)

api.restore_library_block(UsageKey.from_string(self.lib1_problem_block["id"]))

assert event_receiver.call_count == 1
self.assertDictContainsSubset(
{
"signal": LIBRARY_COLLECTION_UPDATED,
"sender": None,
"library_collection": LibraryCollectionData(
self.lib1.library_key,
collection_key=self.col1.key,
background=True,
),
},
event_receiver.call_args_list[0].kwargs,
)

def test_add_component_and_revert(self):
# Add component and publish
api.update_library_collection_components(
Expand Down
1 change: 1 addition & 0 deletions openedx/core/djangoapps/content_libraries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
path('blocks/<str:usage_key_str>/', include([
# Get metadata about a specific XBlock in this library, or delete the block:
path('', views.LibraryBlockView.as_view()),
path('restore/', views.LibraryBlockRestore.as_view()),
# Update collections for a given component
path('collections/', views.LibraryBlockCollectionsView.as_view(), name='update-collections'),
# Get the LTI URL of a specific XBlock
Expand Down
16 changes: 16 additions & 0 deletions openedx/core/djangoapps/content_libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,22 @@ def delete(self, request, usage_key_str): # pylint: disable=unused-argument
return Response({})


@view_auth_classes()
class LibraryBlockRestore(APIView):
"""
View to restore soft-deleted library xblocks.
"""
@convert_exceptions
def post(self, request, usage_key_str) -> Response:
"""
Restores a soft-deleted library block that belongs to a Content Library
"""
key = LibraryUsageLocatorV2.from_string(usage_key_str)
api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
api.restore_library_block(key)
return Response(None, status=status.HTTP_204_NO_CONTENT)


@method_decorator(non_atomic_requests, name="dispatch")
@view_auth_classes()
class LibraryBlockCollectionsView(APIView):
Expand Down
18 changes: 0 additions & 18 deletions openedx/core/djangoapps/content_tagging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
XBLOCK_DUPLICATED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_UPDATED,
LIBRARY_BLOCK_DELETED,
)

from .api import copy_object_tags
Expand All @@ -30,7 +29,6 @@
update_course_tags,
update_xblock_tags,
update_library_block_tags,
delete_library_block_tags,
)
from .toggles import CONTENT_TAGGING_AUTO

Expand Down Expand Up @@ -119,22 +117,6 @@ def auto_tag_library_block(**kwargs):
)


@receiver(LIBRARY_BLOCK_DELETED)
def delete_tag_library_block(**kwargs):
"""
Delete tags associated with a Library XBlock whenever the block is deleted.
"""
library_block_data = kwargs.get("library_block", None)
if not library_block_data or not isinstance(library_block_data, LibraryBlockData):
log.error("Received null or incorrect data for event")
return

try:
delete_library_block_tags(str(library_block_data.usage_key))
except Exception as err: # pylint: disable=broad-except
log.error(f"Failed to delete library block tags: {err}")


@receiver(XBLOCK_DUPLICATED)
def duplicate_tags(**kwargs):
"""
Expand Down
25 changes: 20 additions & 5 deletions openedx/core/djangoapps/content_tagging/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangolib.testing.utils import skip_unless_cms
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
from openedx.core.djangoapps.content_libraries.api import create_library, create_library_block, delete_library_block
from openedx.core.djangoapps.content_libraries.api import (
create_library, create_library_block, delete_library_block, restore_library_block
)

from .. import api
from ..models.base import TaxonomyOrg
Expand Down Expand Up @@ -267,7 +269,7 @@ def test_waffle_disabled_create_delete_xblock(self):
# Still no tags
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)

def test_create_delete_library_block(self):
def test_create_delete_restore_library_block(self):
# Create library
library = create_library(
org=self.orgA,
Expand All @@ -287,11 +289,17 @@ def test_create_delete_library_block(self):
# Check if the tags are created in the Library Block with the user's preferred language
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, 'Português (Brasil)')

# Delete the XBlock
# Soft delete the XBlock
delete_library_block(library_block.usage_key)

# Check if the tags are deleted
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)
# Check that the tags are not deleted
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, 'Português (Brasil)')

# Restore the XBlock
restore_library_block(library_block.usage_key)

# Check if the tags are still present in the Library Block with the user's preferred language
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, 'Português (Brasil)')

@override_waffle_flag(CONTENT_TAGGING_AUTO, active=False)
def test_waffle_disabled_create_delete_library_block(self):
Expand Down Expand Up @@ -319,3 +327,10 @@ def test_waffle_disabled_create_delete_library_block(self):

# Still no tags
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)

# Restore the XBlock
with patch('crum.get_current_request', return_value=fake_request):
restore_library_block(library_block.usage_key)

# Still no tags
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)

0 comments on commit 971afe6

Please sign in to comment.