diff --git a/openedx/core/djangoapps/content/search/documents.py b/openedx/core/djangoapps/content/search/documents.py
index c6ca52098abc..061015751955 100644
--- a/openedx/core/djangoapps/content/search/documents.py
+++ b/openedx/core/djangoapps/content/search/documents.py
@@ -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:
diff --git a/openedx/core/djangoapps/content_libraries/api/containers.py b/openedx/core/djangoapps/content_libraries/api/containers.py
index 6719c07065e4..c03cee2b84d8 100644
--- a/openedx/core/djangoapps/content_libraries/api/containers.py
+++ b/openedx/core/djangoapps/content_libraries/api/containers.py
@@ -13,6 +13,7 @@
LibraryContainerLocator,
LibraryLocatorV2,
UsageKeyV2,
+ LibraryUsageLocatorV2,
)
from openedx_events.content_authoring.data import LibraryContainerData
from openedx_events.content_authoring.signals import (
@@ -42,6 +43,7 @@
"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
+ ]
diff --git a/openedx/core/djangoapps/content_libraries/api/libraries.py b/openedx/core/djangoapps/content_libraries/api/libraries.py
index a4b001cd9a47..3884614ae445 100644
--- a/openedx/core/djangoapps/content_libraries/api/libraries.py
+++ b/openedx/core/djangoapps/content_libraries/api/libraries.py
@@ -82,6 +82,7 @@
ContentLibraryData,
LibraryBlockData,
LibraryCollectionData,
+ LibraryContainerData,
ContentObjectChangedData,
)
from openedx_events.content_authoring.signals import (
@@ -92,6 +93,7 @@
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 @@
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:
"""
diff --git a/openedx/core/djangoapps/content_libraries/library_context.py b/openedx/core/djangoapps/content_libraries/library_context.py
index 4bda10eb12fa..d5f121d8396e 100644
--- a/openedx/core/djangoapps/content_libraries/library_context.py
+++ b/openedx/core/djangoapps/content_libraries/library_context.py
@@ -6,8 +6,8 @@
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 @@ def send_block_updated_event(self, usage_key: UsageKeyV2):
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,
+ )
+ )
diff --git a/openedx/core/djangoapps/content_libraries/tests/test_api.py b/openedx/core/djangoapps/content_libraries/tests/test_api.py
index d4be6fdf6bd9..a1475aa1a039 100644
--- a/openedx/core/djangoapps/content_libraries/tests/test_api.py
+++ b/openedx/core/djangoapps/content_libraries/tests/test_api.py
@@ -16,12 +16,14 @@
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 @@ def test_delete_component_and_revert(self):
},
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 = "Hello world!"
+ 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 = "Hello world!"
+ 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)
diff --git a/openedx/core/djangoapps/xblock/learning_context/learning_context.py b/openedx/core/djangoapps/xblock/learning_context/learning_context.py
index b535e84ca7c1..dc7a21f1c397 100644
--- a/openedx/core/djangoapps/xblock/learning_context/learning_context.py
+++ b/openedx/core/djangoapps/xblock/learning_context/learning_context.py
@@ -76,3 +76,11 @@ def send_block_updated_event(self, usage_key):
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
+ """
diff --git a/openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py b/openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py
index c3885fbf11cc..57582989f60d 100644
--- a/openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py
+++ b/openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py
@@ -317,6 +317,7 @@ def save_block(self, block):
# 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):
"""