feat: Update containers in search index on components update/delete [FC-0083] (#36432)
* feat: Added get_containers_contains_component in containers api with tests * feat: Add publish_status to containers search document * feat: Add LIBRARY_CONTAINER_UPDATED whend deleted a component inside a container * feat: Send LIBRARY_CONTAINER_UPDATED signal when updating component of container * fix: Bugs sending LIBRARY_CONTAINER_UPDATED signal * feat: Add publish_status of container as PublishStatus.Never by default * refactor: ContentLibraryContainersTest to use update_container_children to add components * style: Clean code after fix conflicts * fix: Broken lint * fix: lint
This commit is contained in:
@@ -572,6 +572,7 @@ def searchable_doc_for_container(
|
||||
Fields.usage_key: str(container_key), # Field name isn't exact but this is the closest match
|
||||
Fields.block_id: container_key.container_id, # Field name isn't exact but this is the closest match
|
||||
Fields.access_id: _meili_access_id_from_context_key(container_key.library_key),
|
||||
Fields.publish_status: PublishStatus.never,
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import (
|
||||
LibraryContainerLocator,
|
||||
LibraryLocatorV2,
|
||||
UsageKeyV2,
|
||||
LibraryUsageLocatorV2,
|
||||
)
|
||||
from openedx_events.content_authoring.data import LibraryContainerData
|
||||
from openedx_events.content_authoring.signals import (
|
||||
@@ -42,6 +43,7 @@ __all__ = [
|
||||
"update_container",
|
||||
"delete_container",
|
||||
"update_container_children",
|
||||
"get_containers_contains_component",
|
||||
]
|
||||
|
||||
|
||||
@@ -316,3 +318,20 @@ def update_container_children(
|
||||
)
|
||||
|
||||
return ContainerMetadata.from_container(library_key, new_version.container)
|
||||
|
||||
|
||||
def get_containers_contains_component(
|
||||
usage_key: LibraryUsageLocatorV2
|
||||
) -> list[ContainerMetadata]:
|
||||
"""
|
||||
Get containers that contains the component.
|
||||
"""
|
||||
assert isinstance(usage_key, LibraryUsageLocatorV2)
|
||||
component = get_component_from_usage_key(usage_key)
|
||||
containers = authoring_api.get_containers_with_entity(
|
||||
component.publishable_entity.pk,
|
||||
)
|
||||
return [
|
||||
ContainerMetadata.from_container(usage_key.context_key, container)
|
||||
for container in containers
|
||||
]
|
||||
|
||||
@@ -82,6 +82,7 @@ from openedx_events.content_authoring.data import (
|
||||
ContentLibraryData,
|
||||
LibraryBlockData,
|
||||
LibraryCollectionData,
|
||||
LibraryContainerData,
|
||||
ContentObjectChangedData,
|
||||
)
|
||||
from openedx_events.content_authoring.signals import (
|
||||
@@ -92,6 +93,7 @@ from openedx_events.content_authoring.signals import (
|
||||
LIBRARY_BLOCK_DELETED,
|
||||
LIBRARY_BLOCK_UPDATED,
|
||||
LIBRARY_COLLECTION_UPDATED,
|
||||
LIBRARY_CONTAINER_UPDATED,
|
||||
CONTENT_OBJECT_ASSOCIATIONS_CHANGED,
|
||||
)
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -113,6 +115,7 @@ from openedx.core.djangoapps.xblock.api import (
|
||||
xblock_type_display_name,
|
||||
)
|
||||
from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_learning_core
|
||||
from openedx.core.djangoapps.content_libraries import api as lib_api
|
||||
from openedx.core.types import User as UserType
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
@@ -901,6 +904,18 @@ def set_library_block_olx(usage_key: LibraryUsageLocatorV2, new_olx_str: str) ->
|
||||
)
|
||||
)
|
||||
|
||||
# For each container, trigger LIBRARY_CONTAINER_UPDATED signal and set background=True to trigger
|
||||
# container indexing asynchronously.
|
||||
affected_containers = lib_api.get_containers_contains_component(usage_key)
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=usage_key.lib_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
return new_component_version
|
||||
|
||||
|
||||
@@ -1205,6 +1220,7 @@ def delete_library_block(usage_key: LibraryUsageLocatorV2, remove_from_parent=Tr
|
||||
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)
|
||||
affected_containers = lib_api.get_containers_contains_component(usage_key)
|
||||
|
||||
authoring_api.soft_delete_draft(component.pk)
|
||||
|
||||
@@ -1228,6 +1244,19 @@ def delete_library_block(usage_key: LibraryUsageLocatorV2, remove_from_parent=Tr
|
||||
)
|
||||
)
|
||||
|
||||
# For each container, trigger LIBRARY_CONTAINER_UPDATED signal and set background=True to trigger
|
||||
# container indexing asynchronously.
|
||||
#
|
||||
# To update the components count in containers
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=library_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def restore_library_block(usage_key: LibraryUsageLocatorV2) -> None:
|
||||
"""
|
||||
|
||||
@@ -6,8 +6,8 @@ import logging
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
from openedx_events.content_authoring.data import LibraryBlockData
|
||||
from openedx_events.content_authoring.signals import LIBRARY_BLOCK_UPDATED
|
||||
from openedx_events.content_authoring.data import LibraryBlockData, LibraryContainerData
|
||||
from openedx_events.content_authoring.signals import LIBRARY_BLOCK_UPDATED, LIBRARY_CONTAINER_UPDATED
|
||||
from opaque_keys.edx.keys import UsageKeyV2
|
||||
from opaque_keys.edx.locator import LibraryUsageLocatorV2, LibraryLocatorV2
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -114,3 +114,19 @@ class LibraryContextImpl(LearningContext):
|
||||
usage_key=usage_key,
|
||||
)
|
||||
)
|
||||
|
||||
def send_container_updated_events(self, usage_key: UsageKeyV2):
|
||||
"""
|
||||
Send "container updated" events for containers that contains the library block
|
||||
with the given usage_key.
|
||||
"""
|
||||
assert isinstance(usage_key, LibraryUsageLocatorV2)
|
||||
affected_containers = api.get_containers_contains_component(usage_key)
|
||||
for container in affected_containers:
|
||||
LIBRARY_CONTAINER_UPDATED.send_event(
|
||||
library_container=LibraryContainerData(
|
||||
library_key=usage_key.lib_key,
|
||||
container_key=str(container.container_key),
|
||||
background=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -16,12 +16,14 @@ from opaque_keys.edx.locator import LibraryLocatorV2
|
||||
from openedx_events.content_authoring.data import (
|
||||
ContentObjectChangedData,
|
||||
LibraryCollectionData,
|
||||
LibraryContainerData,
|
||||
)
|
||||
from openedx_events.content_authoring.signals import (
|
||||
CONTENT_OBJECT_ASSOCIATIONS_CHANGED,
|
||||
LIBRARY_COLLECTION_CREATED,
|
||||
LIBRARY_COLLECTION_DELETED,
|
||||
LIBRARY_COLLECTION_UPDATED,
|
||||
LIBRARY_CONTAINER_UPDATED,
|
||||
)
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
from openedx_learning.api import authoring as authoring_api
|
||||
@@ -742,3 +744,109 @@ class ContentLibraryCollectionsTest(ContentLibrariesRestApiTest, OpenEdxEventsTe
|
||||
},
|
||||
event_receiver.call_args_list[1].kwargs,
|
||||
)
|
||||
|
||||
|
||||
class ContentLibraryContainersTest(ContentLibrariesRestApiTest, TestCase):
|
||||
"""
|
||||
Tests for Content Library API containers methods.
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create Content Libraries
|
||||
self._create_library("test-lib-cont-1", "Test Library 1")
|
||||
|
||||
# Fetch the created ContentLibrare objects so we can access their learning_package.id
|
||||
self.lib1 = ContentLibrary.objects.get(slug="test-lib-cont-1")
|
||||
|
||||
# Create Units
|
||||
self.unit1 = api.create_container(self.lib1.library_key, api.ContainerType.Unit, 'unit-1', 'Unit 1', None)
|
||||
self.unit2 = api.create_container(self.lib1.library_key, api.ContainerType.Unit, 'unit-2', 'Unit 2', None)
|
||||
|
||||
# Create XBlocks
|
||||
# Create some library blocks in lib1
|
||||
self.problem_block = self._add_block_to_library(
|
||||
self.lib1.library_key, "problem", "problem1",
|
||||
)
|
||||
self.problem_block_usage_key = UsageKey.from_string(self.problem_block["id"])
|
||||
self.html_block = self._add_block_to_library(
|
||||
self.lib1.library_key, "html", "html1",
|
||||
)
|
||||
self.html_block_usage_key = UsageKey.from_string(self.html_block["id"])
|
||||
|
||||
# Add content to units
|
||||
api.update_container_children(
|
||||
self.unit1.container_key,
|
||||
[self.problem_block_usage_key, self.html_block_usage_key],
|
||||
None,
|
||||
)
|
||||
api.update_container_children(
|
||||
self.unit2.container_key,
|
||||
[self.html_block_usage_key],
|
||||
None,
|
||||
)
|
||||
|
||||
def test_get_containers_contains_component(self):
|
||||
problem_block_containers = api.get_containers_contains_component(self.problem_block_usage_key)
|
||||
html_block_containers = api.get_containers_contains_component(self.html_block_usage_key)
|
||||
|
||||
assert len(problem_block_containers) == 1
|
||||
assert problem_block_containers[0].container_key == self.unit1.container_key
|
||||
|
||||
assert len(html_block_containers) == 2
|
||||
assert html_block_containers[0].container_key == self.unit1.container_key
|
||||
assert html_block_containers[1].container_key == self.unit2.container_key
|
||||
|
||||
def _validate_calls_of_html_block(self, event_mock):
|
||||
"""
|
||||
Validate that the `event_mock` has been called twice
|
||||
using the `LIBRARY_CONTAINER_UPDATED` signal.
|
||||
"""
|
||||
assert event_mock.call_count == 2
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
library_key=self.lib1.library_key,
|
||||
container_key=str(self.unit1.container_key),
|
||||
background=True,
|
||||
)
|
||||
},
|
||||
event_mock.call_args_list[0].kwargs,
|
||||
)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": LIBRARY_CONTAINER_UPDATED,
|
||||
"sender": None,
|
||||
"library_container": LibraryContainerData(
|
||||
library_key=self.lib1.library_key,
|
||||
container_key=str(self.unit2.container_key),
|
||||
background=True,
|
||||
)
|
||||
},
|
||||
event_mock.call_args_list[1].kwargs,
|
||||
)
|
||||
|
||||
def test_call_container_update_signal_when_delete_component(self):
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
api.delete_library_block(self.html_block_usage_key)
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
def test_call_container_update_signal_when_update_olx(self):
|
||||
block_olx = "<html><b>Hello world!</b></html>"
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
self._set_library_block_olx(self.html_block_usage_key, block_olx)
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
def test_call_container_update_signal_when_update_component(self):
|
||||
block_olx = "<html><b>Hello world!</b></html>"
|
||||
container_update_event_receiver = mock.Mock()
|
||||
LIBRARY_CONTAINER_UPDATED.connect(container_update_event_receiver)
|
||||
|
||||
self._set_library_block_fields(self.html_block_usage_key, {"data": block_olx, "metadata": {}})
|
||||
self._validate_calls_of_html_block(container_update_event_receiver)
|
||||
|
||||
@@ -76,3 +76,11 @@ class LearningContext:
|
||||
|
||||
usage_key: the UsageKeyV2 subclass used for this learning context
|
||||
"""
|
||||
|
||||
def send_container_updated_events(self, usage_key):
|
||||
"""
|
||||
Send "container updated" events for containers that contains the block with
|
||||
the given usage_key in this context.
|
||||
|
||||
usage_key: the UsageKeyV2 subclass used for this learning context
|
||||
"""
|
||||
|
||||
@@ -317,6 +317,7 @@ class LearningCoreXBlockRuntime(XBlockRuntime):
|
||||
# Signal that we've modified this block
|
||||
learning_context = get_learning_context_impl(usage_key)
|
||||
learning_context.send_block_updated_event(usage_key)
|
||||
learning_context.send_container_updated_events(usage_key)
|
||||
|
||||
def _get_component_from_usage_key(self, usage_key):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user