Skip to content
Merged
1 change: 1 addition & 0 deletions openedx/core/djangoapps/content/search/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
LibraryContainerLocator,
LibraryLocatorV2,
UsageKeyV2,
LibraryUsageLocatorV2,
)
from openedx_events.content_authoring.data import LibraryContainerData
from openedx_events.content_authoring.signals import (
Expand Down Expand Up @@ -42,6 +43,7 @@
"update_container",
"delete_container",
"update_container_children",
"get_containers_contains_component",
]


Expand Down Expand Up @@ -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
]
29 changes: 29 additions & 0 deletions openedx/core/djangoapps/content_libraries/api/libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
ContentLibraryData,
LibraryBlockData,
LibraryCollectionData,
LibraryContainerData,
ContentObjectChangedData,
)
from openedx_events.content_authoring.signals import (
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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)

Expand All @@ -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:
"""
Expand Down
20 changes: 18 additions & 2 deletions openedx/core/djangoapps/content_libraries/library_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
)
108 changes: 108 additions & 0 deletions openedx/core/djangoapps/content_libraries/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = "<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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
Loading