Skip to content

Commit 971afe6

Browse files
authored
feat: api to restore soft-deleted component [FC-0076] (#35993)
Adds API to handle restoring soft-deleted library blocks.
1 parent f4d110c commit 971afe6

File tree

8 files changed

+120
-24
lines changed

8 files changed

+120
-24
lines changed

openedx/core/djangoapps/content/search/documents.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ def _collections_for_content_object(object_id: UsageKey | LearningContextKey) ->
306306
307307
If the object is in no collections, returns:
308308
{
309-
"collections": {},
309+
"collections": {
310+
"display_name": [],
311+
"key": [],
312+
},
310313
}
311314
312315
"""

openedx/core/djangoapps/content/search/tests/test_handlers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,13 @@ def test_create_delete_library_block(self, meilisearch_client):
185185
meilisearch_client.return_value.index.return_value.delete_document.assert_called_with(
186186
"lborgalib_aproblemproblem1-ca3186e9"
187187
)
188+
189+
# Restore the Library Block
190+
library_api.restore_library_block(problem.usage_key)
191+
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call([doc_problem])
192+
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call(
193+
[{'id': doc_problem['id'], 'collections': {'display_name': [], 'key': []}}]
194+
)
195+
meilisearch_client.return_value.index.return_value.update_documents.assert_any_call(
196+
[{'id': doc_problem['id'], 'tags': {}}]
197+
)

openedx/core/djangoapps/content_libraries/api.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,46 @@ def delete_library_block(usage_key, remove_from_parent=True):
11331133
)
11341134

11351135

1136+
def restore_library_block(usage_key):
1137+
"""
1138+
Restore the specified library block.
1139+
"""
1140+
component = get_component_from_usage_key(usage_key)
1141+
library_key = usage_key.context_key
1142+
affected_collections = authoring_api.get_entity_collections(component.learning_package_id, component.key)
1143+
1144+
# Set draft version back to the latest available component version id.
1145+
authoring_api.set_draft_version(component.pk, component.versioning.latest.pk)
1146+
1147+
LIBRARY_BLOCK_CREATED.send_event(
1148+
library_block=LibraryBlockData(
1149+
library_key=library_key,
1150+
usage_key=usage_key
1151+
)
1152+
)
1153+
1154+
# Add tags and collections back to index
1155+
CONTENT_OBJECT_ASSOCIATIONS_CHANGED.send_event(
1156+
content_object=ContentObjectChangedData(
1157+
object_id=str(usage_key),
1158+
changes=["collections", "tags"],
1159+
),
1160+
)
1161+
1162+
# For each collection, trigger LIBRARY_COLLECTION_UPDATED signal and set background=True to trigger
1163+
# collection indexing asynchronously.
1164+
#
1165+
# To restore the component in the collections
1166+
for collection in affected_collections:
1167+
LIBRARY_COLLECTION_UPDATED.send_event(
1168+
library_collection=LibraryCollectionData(
1169+
library_key=library_key,
1170+
collection_key=collection.key,
1171+
background=True,
1172+
)
1173+
)
1174+
1175+
11361176
def get_library_block_static_asset_files(usage_key) -> list[LibraryXBlockStaticFile]:
11371177
"""
11381178
Given an XBlock in a content library, list all the static asset files

openedx/core/djangoapps/content_libraries/tests/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,35 @@ def test_delete_library_block(self):
591591
event_receiver.call_args_list[0].kwargs,
592592
)
593593

594+
def test_restore_library_block(self):
595+
api.update_library_collection_components(
596+
self.lib1.library_key,
597+
self.col1.key,
598+
usage_keys=[
599+
UsageKey.from_string(self.lib1_problem_block["id"]),
600+
UsageKey.from_string(self.lib1_html_block["id"]),
601+
],
602+
)
603+
604+
event_receiver = mock.Mock()
605+
LIBRARY_COLLECTION_UPDATED.connect(event_receiver)
606+
607+
api.restore_library_block(UsageKey.from_string(self.lib1_problem_block["id"]))
608+
609+
assert event_receiver.call_count == 1
610+
self.assertDictContainsSubset(
611+
{
612+
"signal": LIBRARY_COLLECTION_UPDATED,
613+
"sender": None,
614+
"library_collection": LibraryCollectionData(
615+
self.lib1.library_key,
616+
collection_key=self.col1.key,
617+
background=True,
618+
),
619+
},
620+
event_receiver.call_args_list[0].kwargs,
621+
)
622+
594623
def test_add_component_and_revert(self):
595624
# Add component and publish
596625
api.update_library_collection_components(

openedx/core/djangoapps/content_libraries/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
path('blocks/<str:usage_key_str>/', include([
5858
# Get metadata about a specific XBlock in this library, or delete the block:
5959
path('', views.LibraryBlockView.as_view()),
60+
path('restore/', views.LibraryBlockRestore.as_view()),
6061
# Update collections for a given component
6162
path('collections/', views.LibraryBlockCollectionsView.as_view(), name='update-collections'),
6263
# Get the LTI URL of a specific XBlock

openedx/core/djangoapps/content_libraries/views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,22 @@ def delete(self, request, usage_key_str): # pylint: disable=unused-argument
644644
return Response({})
645645

646646

647+
@view_auth_classes()
648+
class LibraryBlockRestore(APIView):
649+
"""
650+
View to restore soft-deleted library xblocks.
651+
"""
652+
@convert_exceptions
653+
def post(self, request, usage_key_str) -> Response:
654+
"""
655+
Restores a soft-deleted library block that belongs to a Content Library
656+
"""
657+
key = LibraryUsageLocatorV2.from_string(usage_key_str)
658+
api.require_permission_for_library_key(key.lib_key, request.user, permissions.CAN_EDIT_THIS_CONTENT_LIBRARY)
659+
api.restore_library_block(key)
660+
return Response(None, status=status.HTTP_204_NO_CONTENT)
661+
662+
647663
@method_decorator(non_atomic_requests, name="dispatch")
648664
@view_auth_classes()
649665
class LibraryBlockCollectionsView(APIView):

openedx/core/djangoapps/content_tagging/handlers.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
XBLOCK_DUPLICATED,
2121
LIBRARY_BLOCK_CREATED,
2222
LIBRARY_BLOCK_UPDATED,
23-
LIBRARY_BLOCK_DELETED,
2423
)
2524

2625
from .api import copy_object_tags
@@ -30,7 +29,6 @@
3029
update_course_tags,
3130
update_xblock_tags,
3231
update_library_block_tags,
33-
delete_library_block_tags,
3432
)
3533
from .toggles import CONTENT_TAGGING_AUTO
3634

@@ -119,22 +117,6 @@ def auto_tag_library_block(**kwargs):
119117
)
120118

121119

122-
@receiver(LIBRARY_BLOCK_DELETED)
123-
def delete_tag_library_block(**kwargs):
124-
"""
125-
Delete tags associated with a Library XBlock whenever the block is deleted.
126-
"""
127-
library_block_data = kwargs.get("library_block", None)
128-
if not library_block_data or not isinstance(library_block_data, LibraryBlockData):
129-
log.error("Received null or incorrect data for event")
130-
return
131-
132-
try:
133-
delete_library_block_tags(str(library_block_data.usage_key))
134-
except Exception as err: # pylint: disable=broad-except
135-
log.error(f"Failed to delete library block tags: {err}")
136-
137-
138120
@receiver(XBLOCK_DUPLICATED)
139121
def duplicate_tags(**kwargs):
140122
"""

openedx/core/djangoapps/content_tagging/tests/test_tasks.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from common.djangoapps.student.tests.factories import UserFactory
1515
from openedx.core.djangolib.testing.utils import skip_unless_cms
1616
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
17-
from openedx.core.djangoapps.content_libraries.api import create_library, create_library_block, delete_library_block
17+
from openedx.core.djangoapps.content_libraries.api import (
18+
create_library, create_library_block, delete_library_block, restore_library_block
19+
)
1820

1921
from .. import api
2022
from ..models.base import TaxonomyOrg
@@ -267,7 +269,7 @@ def test_waffle_disabled_create_delete_xblock(self):
267269
# Still no tags
268270
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)
269271

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

290-
# Delete the XBlock
292+
# Soft delete the XBlock
291293
delete_library_block(library_block.usage_key)
292294

293-
# Check if the tags are deleted
294-
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)
295+
# Check that the tags are not deleted
296+
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, 'Português (Brasil)')
297+
298+
# Restore the XBlock
299+
restore_library_block(library_block.usage_key)
300+
301+
# Check if the tags are still present in the Library Block with the user's preferred language
302+
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, 'Português (Brasil)')
295303

296304
@override_waffle_flag(CONTENT_TAGGING_AUTO, active=False)
297305
def test_waffle_disabled_create_delete_library_block(self):
@@ -319,3 +327,10 @@ def test_waffle_disabled_create_delete_library_block(self):
319327

320328
# Still no tags
321329
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)
330+
331+
# Restore the XBlock
332+
with patch('crum.get_current_request', return_value=fake_request):
333+
restore_library_block(library_block.usage_key)
334+
335+
# Still no tags
336+
assert self._check_tag(usage_key_str, LANGUAGE_TAXONOMY_ID, None)

0 commit comments

Comments
 (0)