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:
Chris Chávez
2025-04-02 08:16:41 -05:00
committed by GitHub
parent 3bda03fa4b
commit 1c14c3a518
7 changed files with 184 additions and 2 deletions

View File

@@ -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:

View File

@@ -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
]

View File

@@ -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:
"""

View File

@@ -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,
)
)

View File

@@ -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)

View File

@@ -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
"""

View File

@@ -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):
"""