feat!: add new content authoring event signals

This commit is contained in:
Rômulo Penido
2023-07-24 14:55:58 -03:00
committed by Navin Karkera
parent 02127673f8
commit c54070989b
10 changed files with 631 additions and 77 deletions

View File

@@ -192,18 +192,55 @@ Content Authoring Events
- *Type*
- *Date added*
* - `COURSE_CATALOG_INFO_CHANGED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L23>`_
* - `COURSE_CATALOG_INFO_CHANGED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L25>`_
- org.openedx.content_authoring.course.catalog_info.changed.v1
- `2022-08-24 <https://github.com/openedx/edx-platform/blob/a8598fa1fac5e26ac212aa588e8527e727581742/cms/djangoapps/contentstore/signals/handlers.py#L111>`_
* - `XBLOCK_PUBLISHED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L30>`_
* - `XBLOCK_PUBLISHED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L63>`_
- org.openedx.content_authoring.xblock.published.v1
- `2022-12-06 <https://github.com/openedx/edx-platform/blob/master/xmodule/modulestore/mixed.py#L926>`_
* - `XBLOCK_DELETED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L42>`_
* - `XBLOCK_DELETED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L75>`_
- org.openedx.content_authoring.xblock.deleted.v1
- `2022-12-06 <https://github.com/openedx/edx-platform/blob/master/xmodule/modulestore/mixed.py#L804>`_
* - `XBLOCK_DUPLICATED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L54>`_
* - `XBLOCK_DUPLICATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L87>`_
- org.openedx.content_authoring.xblock.duplicated.v1
- `2022-12-06 <https://github.com/openedx/edx-platform/blob/master/cms/djangoapps/contentstore/views/item.py#L965>`_
* - `XBLOCK_CREATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L36>`_
- org.openedx.content_authoring.xblock.created.v1
- 2023-07-20
* - `XBLOCK_UPDATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L47>`_
- org.openedx.content_authoring.xblock.updated.v1
- 2023-07-20
* - `COURSE_CREATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L123>`_
- org.openedx.content_authoring.course.created.v1
- 2023-07-20
* - `CONTENT_LIBRARY_CREATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L134>`_
- org.openedx.content_authoring.content_library.created.v1
- 2023-07-20
* - `CONTENT_LIBRARY_UPDATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L145>`_
- org.openedx.content_authoring.content_library.updated.v1
- 2023-07-20
* - `CONTENT_LIBRARY_DELETED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L156>`_
- org.openedx.content_authoring.content_library.deleted.v1
- 2023-07-20
* - `LIBRARY_BLOCK_CREATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L167>`_
- org.openedx.content_authoring.content_library.created.v1
- 2023-07-20
* - `LIBRARY_BLOCK_UPDATED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L178>`_
- org.openedx.content_authoring.content_library.updated.v1
- 2023-07-20
* - `LIBRARY_BLOCK_DELETED <https://github.com/openedx/openedx-events/blob/c0eb4ba1a3d7d066d58e5c87920b8ccb0645f769/openedx_events/content_authoring/signals.py#L189>`_
- org.openedx.content_authoring.content_library.deleted.v1
- 2023-07-20

View File

@@ -76,6 +76,15 @@ from opaque_keys.edx.locator import (
LibraryUsageLocatorV2,
LibraryLocator as LibraryLocatorV1
)
from openedx_events.content_authoring.data import ContentLibraryData, LibraryBlockData
from openedx_events.content_authoring.signals import (
CONTENT_LIBRARY_CREATED,
CONTENT_LIBRARY_DELETED,
CONTENT_LIBRARY_UPDATED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_DELETED,
LIBRARY_BLOCK_UPDATED,
)
from organizations.models import Organization
from xblock.core import XBlock
@@ -90,14 +99,7 @@ from openedx.core.djangoapps.content_libraries.models import (
ContentLibraryPermission,
ContentLibraryBlockImportTask,
)
from openedx.core.djangoapps.content_libraries.signals import (
CONTENT_LIBRARY_CREATED,
CONTENT_LIBRARY_UPDATED,
CONTENT_LIBRARY_DELETED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_UPDATED,
LIBRARY_BLOCK_DELETED,
)
from openedx.core.djangoapps.xblock.api import (
get_block_display_name,
get_learning_context_impl,
@@ -452,7 +454,11 @@ def create_library(
)
except IntegrityError:
raise LibraryAlreadyExists(slug) # lint-amnesty, pylint: disable=raise-missing-from
CONTENT_LIBRARY_CREATED.send(sender=None, library_key=ref.library_key)
CONTENT_LIBRARY_CREATED.send_event(
content_library=ContentLibraryData(
library_key=ref.library_key
)
)
return ContentLibraryMetadata(
key=ref.library_key,
bundle_uuid=bundle.uuid,
@@ -602,7 +608,11 @@ def update_library(
assert isinstance(description, str)
fields["description"] = description
update_bundle(ref.bundle_uuid, **fields)
CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=ref.library_key)
CONTENT_LIBRARY_UPDATED.send_event(
content_library=ContentLibraryData(
library_key=ref.library_key
)
)
def delete_library(library_key):
@@ -617,7 +627,11 @@ def delete_library(library_key):
# system, which is a better state than having a reference to a library with
# no backing blockstore bundle.
ref.delete()
CONTENT_LIBRARY_DELETED.send(sender=None, library_key=ref.library_key)
CONTENT_LIBRARY_DELETED.send_event(
content_library=ContentLibraryData(
library_key=ref.library_key
)
)
try:
delete_bundle(bundle_uuid)
except:
@@ -754,7 +768,12 @@ def set_library_block_olx(usage_key, new_olx_str):
write_draft_file(draft.uuid, metadata.def_key.olx_path, new_olx_str.encode('utf-8'))
# Clear the bundle cache so everyone sees the new block immediately:
BundleCache(metadata.def_key.bundle_uuid, draft_name=DRAFT_NAME).clear()
LIBRARY_BLOCK_UPDATED.send(sender=None, library_key=usage_key.context_key, usage_key=usage_key)
LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=usage_key.context_key,
usage_key=usage_key
)
)
def create_library_block(library_key, block_type, definition_id):
@@ -803,7 +822,12 @@ def create_library_block(library_key, block_type, definition_id):
# Clear the bundle cache so everyone sees the new block immediately:
BundleCache(ref.bundle_uuid, draft_name=DRAFT_NAME).clear()
# Now return the metadata about the new block:
LIBRARY_BLOCK_CREATED.send(sender=None, library_key=ref.library_key, usage_key=usage_key)
LIBRARY_BLOCK_CREATED.send_event(
library_block=LibraryBlockData(
library_key=ref.library_key,
usage_key=usage_key
)
)
return get_library_block(usage_key)
@@ -856,7 +880,12 @@ def delete_library_block(usage_key, remove_from_parent=True):
pass
# Clear the bundle cache so everyone sees the deleted block immediately:
lib_bundle.cache.clear()
LIBRARY_BLOCK_DELETED.send(sender=None, library_key=lib_bundle.library_key, usage_key=usage_key)
LIBRARY_BLOCK_DELETED.send_event(
library_block=LibraryBlockData(
library_key=lib_bundle.library_key,
usage_key=usage_key
)
)
def create_library_block_child(parent_usage_key, block_type, definition_id):
@@ -880,7 +909,12 @@ def create_library_block_child(parent_usage_key, block_type, definition_id):
parent_block.runtime.add_child_include(parent_block, include_data)
parent_block.save()
ref = ContentLibrary.objects.get_by_key(parent_usage_key.context_key)
LIBRARY_BLOCK_UPDATED.send(sender=None, library_key=ref.library_key, usage_key=metadata.usage_key)
LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=ref.library_key,
usage_key=metadata.usage_key
)
)
return metadata
@@ -930,7 +964,12 @@ def add_library_block_static_asset_file(usage_key, file_name, file_content):
file_metadata = blockstore_cache.get_bundle_file_metadata_with_cache(
bundle_uuid=def_key.bundle_uuid, path=file_path, draft_name=DRAFT_NAME,
)
LIBRARY_BLOCK_UPDATED.send(sender=None, library_key=lib_bundle.library_key, usage_key=usage_key)
LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=lib_bundle.library_key,
usage_key=usage_key
)
)
return LibraryXBlockStaticFile(path=file_metadata.path, url=file_metadata.url, size=file_metadata.size)
@@ -951,7 +990,12 @@ def delete_library_block_static_asset_file(usage_key, file_name):
write_draft_file(draft.uuid, file_path, contents=None)
# Clear the bundle cache so everyone sees the new file immediately:
lib_bundle.cache.clear()
LIBRARY_BLOCK_UPDATED.send(sender=None, library_key=lib_bundle.library_key, usage_key=usage_key)
LIBRARY_BLOCK_UPDATED.send_event(
library_block=LibraryBlockData(
library_key=lib_bundle.library_key,
usage_key=usage_key
)
)
def get_allowed_block_types(library_key): # pylint: disable=unused-argument
@@ -1044,7 +1088,11 @@ def create_bundle_link(library_key, link_id, target_opaque_key, version=None):
set_draft_link(draft.uuid, link_id, target_bundle_uuid, version)
# Clear the cache:
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key)
CONTENT_LIBRARY_UPDATED.send_event(
content_library=ContentLibraryData(
library_key=library_key
)
)
def update_bundle_link(library_key, link_id, version=None, delete=False):
@@ -1068,7 +1116,11 @@ def update_bundle_link(library_key, link_id, version=None, delete=False):
set_draft_link(draft.uuid, link_id, link.bundle_uuid, version)
# Clear the cache:
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key)
CONTENT_LIBRARY_UPDATED.send_event(
content_library=ContentLibraryData(
library_key=library_key
)
)
def publish_changes(library_key):
@@ -1084,7 +1136,12 @@ def publish_changes(library_key):
return # If there is no draft, no action is needed.
LibraryBundle(library_key, ref.bundle_uuid).cache.clear()
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key, update_blocks=True)
CONTENT_LIBRARY_UPDATED.send_event(
content_library=ContentLibraryData(
library_key=library_key,
update_blocks=True
)
)
def revert_changes(library_key):
@@ -1100,7 +1157,12 @@ def revert_changes(library_key):
else:
return # If there is no draft, no action is needed.
LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key, update_blocks=True)
CONTENT_LIBRARY_UPDATED.send_event(
content_library=ContentLibraryData(
library_key=library_key,
update_blocks=True
)
)
# Import from Courseware

View File

@@ -9,16 +9,17 @@ from elasticsearch.exceptions import ConnectionError as ElasticConnectionError
from search.elastic import _translate_hits, RESERVED_CHARACTERS
from search.search_engine_base import SearchEngine
from opaque_keys.edx.locator import LibraryUsageLocatorV2
from openedx_events.content_authoring.data import ContentLibraryData, LibraryBlockData
from openedx_events.content_authoring.signals import (
CONTENT_LIBRARY_CREATED,
CONTENT_LIBRARY_DELETED,
CONTENT_LIBRARY_UPDATED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_DELETED,
LIBRARY_BLOCK_UPDATED,
)
from openedx.core.djangoapps.content_libraries.constants import DRAFT_NAME
from openedx.core.djangoapps.content_libraries.signals import (
CONTENT_LIBRARY_CREATED,
CONTENT_LIBRARY_UPDATED,
CONTENT_LIBRARY_DELETED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_UPDATED,
LIBRARY_BLOCK_DELETED,
)
from openedx.core.djangoapps.content_libraries.library_bundle import LibraryBundle
from openedx.core.djangoapps.content_libraries.models import ContentLibrary
from openedx.core.lib.blockstore_api import get_bundle
@@ -242,17 +243,21 @@ class LibraryBlockIndexer(SearchIndexerBase):
@receiver(CONTENT_LIBRARY_CREATED)
@receiver(CONTENT_LIBRARY_UPDATED)
@receiver(LIBRARY_BLOCK_CREATED)
@receiver(LIBRARY_BLOCK_UPDATED)
@receiver(LIBRARY_BLOCK_DELETED)
def index_library(sender, library_key, **kwargs): # pylint: disable=unused-argument
def index_library(**kwargs):
"""
Index library when created or updated, or when its blocks are modified.
"""
content_library = kwargs.get('content_library', None)
if not content_library or not isinstance(content_library, ContentLibraryData):
log.error('Received null or incorrect data for event')
return
library_key = content_library.library_key
update_blocks = content_library.update_blocks
if ContentLibraryIndexer.indexing_is_enabled():
try:
ContentLibraryIndexer.index_items([library_key])
if kwargs.get('update_blocks', False):
if update_blocks:
blocks = LibraryBlockIndexer.get_items(filter_terms={
'library_key': str(library_key)
})
@@ -262,12 +267,38 @@ def index_library(sender, library_key, **kwargs): # pylint: disable=unused-argu
log.exception(e)
@receiver(LIBRARY_BLOCK_CREATED)
@receiver(LIBRARY_BLOCK_DELETED)
@receiver(LIBRARY_BLOCK_UPDATED)
def index_library_block(**kwargs):
"""
Index library when its blocks are created, modified, or deleted.
"""
library_block = kwargs.get('library_block', None)
if not library_block or not isinstance(library_block, LibraryBlockData):
log.error('Received null or incorrect data for event')
return
library_key = library_block.library_key
if ContentLibraryIndexer.indexing_is_enabled():
try:
ContentLibraryIndexer.index_items([library_key])
except ElasticConnectionError as e:
log.exception(e)
@receiver(CONTENT_LIBRARY_DELETED)
def remove_library_index(sender, library_key, **kwargs): # pylint: disable=unused-argument
def remove_library_index(**kwargs):
"""
Remove from index when library is deleted
"""
content_library = kwargs.get('content_library', None)
if not content_library or not isinstance(content_library, ContentLibraryData):
log.error('Received null or incorrect data for event')
return
if ContentLibraryIndexer.indexing_is_enabled():
library_key = content_library.library_key
try:
ContentLibraryIndexer.remove_items([library_key])
blocks = LibraryBlockIndexer.get_items(filter_terms={
@@ -280,10 +311,16 @@ def remove_library_index(sender, library_key, **kwargs): # pylint: disable=unus
@receiver(LIBRARY_BLOCK_CREATED)
@receiver(LIBRARY_BLOCK_UPDATED)
def index_block(sender, usage_key, **kwargs): # pylint: disable=unused-argument
def index_block(**kwargs):
"""
Index block metadata when created
Index block metadata when created or updated
"""
library_block = kwargs.get('library_block', None)
if not library_block or not isinstance(library_block, LibraryBlockData):
log.error('Received null or incorrect data for event')
return
usage_key = library_block.usage_key
if LibraryBlockIndexer.indexing_is_enabled():
try:
LibraryBlockIndexer.index_items([usage_key])
@@ -292,10 +329,16 @@ def index_block(sender, usage_key, **kwargs): # pylint: disable=unused-argument
@receiver(LIBRARY_BLOCK_DELETED)
def remove_block_index(sender, usage_key, **kwargs): # pylint: disable=unused-argument
def remove_block_index(**kwargs):
"""
Remove the block from the index when deleted
"""
library_block = kwargs.get('library_block', None)
if not library_block or not isinstance(library_block, LibraryBlockData):
log.error('Received null or incorrect data for LIBRARY_BLOCK_DELETED')
return
usage_key = library_block.usage_key
if LibraryBlockIndexer.indexing_is_enabled():
try:
LibraryBlockIndexer.remove_items([usage_key])

View File

@@ -1,17 +0,0 @@
"""
Content libraries related signals.
"""
from django.dispatch import Signal
# providing_args=['library_key']
CONTENT_LIBRARY_CREATED = Signal()
# providing_args=['library_key', 'update_blocks']
CONTENT_LIBRARY_UPDATED = Signal()
# providing_args=['library_key']
CONTENT_LIBRARY_DELETED = Signal()
# Same providing_args=['library_key', 'usage_key'] for next 3 signals.
LIBRARY_BLOCK_CREATED = Signal()
LIBRARY_BLOCK_DELETED = Signal()
LIBRARY_BLOCK_UPDATED = Signal()

View File

@@ -331,7 +331,7 @@ class _ContentLibrariesRestApiTestMixin:
assert response.status_code == expect_response,\
'Unexpected response code {}:\n{}'.format(response.status_code, getattr(response, 'data', '(no data)'))
def _delete_library_block_asset(self, block_key, file_name, expect_response=200):
def _delete_library_block_asset(self, block_key, file_name, expect_response=204):
""" Delete a static asset file. """
url = URL_LIB_BLOCK_ASSET_FILE.format(block_key=block_key, file_name=file_name)
return self._api('delete', url, None, expect_response)

View File

@@ -2,7 +2,7 @@
Tests for Blockstore-based Content Libraries
"""
from uuid import UUID
from unittest.mock import patch
from unittest.mock import Mock, patch
import ddt
from django.conf import settings
@@ -12,6 +12,16 @@ from django.test.utils import override_settings
from organizations.models import Organization
from rest_framework.test import APITestCase
from opaque_keys.edx.locator import LibraryLocatorV2, LibraryUsageLocatorV2
from openedx_events.content_authoring.data import ContentLibraryData, LibraryBlockData
from openedx_events.content_authoring.signals import (
CONTENT_LIBRARY_CREATED,
CONTENT_LIBRARY_DELETED,
CONTENT_LIBRARY_UPDATED,
LIBRARY_BLOCK_CREATED,
LIBRARY_BLOCK_DELETED,
LIBRARY_BLOCK_UPDATED,
)
from openedx.core.djangoapps.content_libraries.libraries_index import LibraryBlockIndexer, ContentLibraryIndexer
from openedx.core.djangoapps.content_libraries.tests.base import (
ContentLibrariesRestApiBlockstoreServiceTest,
@@ -485,7 +495,7 @@ class ContentLibrariesTestMixin:
assert len(self._get_library_blocks(lib['id'], {'text_search': 'Foo', 'block_type': 'video'})) == 0
assert len(self._get_library_blocks(lib['id'], {'text_search': 'Baz', 'block_type': 'video'})) == 1
assert len(self._get_library_blocks(lib['id'], {'text_search': 'Baz', 'block_type': ['video', 'html']})) ==\
2
2
assert len(self._get_library_blocks(lib['id'], {'block_type': 'video'})) == 1
assert len(self._get_library_blocks(lib['id'], {'block_type': 'problem'})) == 3
assert len(self._get_library_blocks(lib['id'], {'block_type': 'squirrel'})) == 0
@@ -537,8 +547,8 @@ class ContentLibrariesTestMixin:
# Check the resulting OLX of the unit:
assert self._get_library_block_olx(unit_block['id']) ==\
'<unit xblock-family="xblock.v1">\n <xblock-include definition="html/html1"/>\n' \
' <xblock-include definition="problem/problem1"/>\n</unit>\n'
'<unit xblock-family="xblock.v1">\n <xblock-include definition="html/html1"/>\n' \
' <xblock-include definition="problem/problem1"/>\n</unit>\n'
# The unit can see and render its children:
fragment = self._render_block_view(unit_block["id"], "student_view")
@@ -932,6 +942,295 @@ class ContentLibrariesTestMixin:
else:
assert len(types) > 1
def test_content_library_create_event(self):
"""
Check that CONTENT_LIBRARY_CREATED event is sent when a content library is created.
"""
event_receiver = Mock()
CONTENT_LIBRARY_CREATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_event_create", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
library_key = LibraryLocatorV2.from_string(lib['id'])
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": CONTENT_LIBRARY_CREATED,
"sender": None,
"content_library": ContentLibraryData(
library_key=library_key,
update_blocks=False,
),
},
event_receiver.call_args.kwargs
)
def test_content_library_update_event(self):
"""
Check that CONTENT_LIBRARY_UPDATED event is sent when a content library is updated.
"""
event_receiver = Mock()
CONTENT_LIBRARY_UPDATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_event_update", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib2 = self._update_library(lib["id"], title="New Title")
library_key = LibraryLocatorV2.from_string(lib2['id'])
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": CONTENT_LIBRARY_UPDATED,
"sender": None,
"content_library": ContentLibraryData(
library_key=library_key,
update_blocks=False,
),
},
event_receiver.call_args.kwargs
)
def test_content_library_delete_event(self):
"""
Check that CONTENT_LIBRARY_DELETED event is sent when a content library is deleted.
"""
event_receiver = Mock()
CONTENT_LIBRARY_DELETED.connect(event_receiver)
lib = self._create_library(slug="test_lib_event_delete", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
library_key = LibraryLocatorV2.from_string(lib['id'])
self._delete_library(lib["id"])
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": CONTENT_LIBRARY_DELETED,
"sender": None,
"content_library": ContentLibraryData(
library_key=library_key,
update_blocks=False,
),
},
event_receiver.call_args.kwargs
)
def test_library_block_create_event(self):
"""
Check that LIBRARY_BLOCK_CREATED event is sent when a library block is created.
"""
event_receiver = Mock()
LIBRARY_BLOCK_CREATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_create", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
self._add_block_to_library(lib_id, "problem", "problem1")
library_key = LibraryLocatorV2.from_string(lib_id)
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="problem",
usage_id="problem1"
)
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_CREATED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
def test_library_block_olx_update_event(self):
"""
Check that LIBRARY_BLOCK_CREATED event is sent when the OLX source is updated.
"""
event_receiver = Mock()
LIBRARY_BLOCK_UPDATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_olx_update", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
library_key = LibraryLocatorV2.from_string(lib_id)
block = self._add_block_to_library(lib_id, "problem", "problem1")
block_id = block["id"]
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="problem",
usage_id="problem1"
)
new_olx = """
<problem display_name="New Multi Choice Question" max_attempts="5">
<multiplechoiceresponse>
<p>This is a normal capa problem with unicode 🔥. It has "maximum attempts" set to **5**.</p>
<label>Blockstore is designed to store.</label>
<choicegroup type="MultipleChoice">
<choice correct="false">XBlock metadata only</choice>
<choice correct="true">XBlock data/metadata and associated static asset files</choice>
<choice correct="false">Static asset files for XBlocks and courseware</choice>
<choice correct="false">XModule metadata only</choice>
</choicegroup>
</multiplechoiceresponse>
</problem>
""".strip()
self._set_library_block_olx(block_id, new_olx)
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_UPDATED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
def test_library_block_child_update_event(self):
"""
Check that LIBRARY_BLOCK_CREATED event is sent when a child is created.
"""
event_receiver = Mock()
LIBRARY_BLOCK_UPDATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_child_update", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
library_key = LibraryLocatorV2.from_string(lib_id)
parent_block = self._add_block_to_library(lib_id, "unit", "u1")
parent_block_id = parent_block["id"]
self._add_block_to_library(lib["id"], "problem", "problem1", parent_block=parent_block_id)
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="problem",
usage_id="problem1"
)
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_UPDATED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
def test_library_block_add_asset_update_event(self):
"""
Check that LIBRARY_BLOCK_CREATED event is sent when a static asset is uploaded associated with the XBlock.
"""
event_receiver = Mock()
LIBRARY_BLOCK_UPDATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_add_asset_update", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
library_key = LibraryLocatorV2.from_string(lib_id)
block = self._add_block_to_library(lib_id, "unit", "u1")
block_id = block["id"]
self._set_library_block_asset(block_id, "test.txt", b"data")
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="unit",
usage_id="u1"
)
event_receiver.assert_called_once()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_UPDATED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
def test_library_block_del_asset_update_event(self):
"""
Check that LIBRARY_BLOCK_CREATED event is sent when a static asset is removed from XBlock.
"""
event_receiver = Mock()
LIBRARY_BLOCK_UPDATED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_del_asset_update", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
library_key = LibraryLocatorV2.from_string(lib_id)
block = self._add_block_to_library(lib_id, "unit", "u1")
block_id = block["id"]
self._set_library_block_asset(block_id, "test.txt", b"data")
self._delete_library_block_asset(block_id, 'text.txt')
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="unit",
usage_id="u1"
)
event_receiver.assert_called()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_UPDATED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
def test_library_block_delete_event(self):
"""
Check that LIBRARY_BLOCK_DELETED event is sent when a content library is deleted.
"""
event_receiver = Mock()
LIBRARY_BLOCK_DELETED.connect(event_receiver)
lib = self._create_library(slug="test_lib_block_event_delete", title="Event Test Library", description="Testing event in library") # lint-amnesty, pylint: disable=line-too-long
lib_id = lib["id"]
library_key = LibraryLocatorV2.from_string(lib_id)
block = self._add_block_to_library(lib_id, "problem", "problem1")
block_id = block['id']
usage_key = LibraryUsageLocatorV2(
lib_key=library_key,
block_type="problem",
usage_id="problem1"
)
self._delete_library_block(block_id)
event_receiver.assert_called()
self.assertDictContainsSubset(
{
"signal": LIBRARY_BLOCK_DELETED,
"sender": None,
"library_block": LibraryBlockData(
library_key=library_key,
usage_key=usage_key
),
},
event_receiver.call_args.kwargs
)
@elasticsearch_test
class ContentLibrariesBlockstoreServiceTest(

View File

@@ -165,7 +165,7 @@ class ContentLibraryIndexerTestMixin:
self._set_library_block_asset(block["id"], "whatever.png", b"data")
verify_uncommitted_libraries(library_key, True, False)
commit_library_and_verify(library_key)
self._delete_library_block_asset(block["id"], "whatever.png", expect_response=204)
self._delete_library_block_asset(block["id"], "whatever.png")
verify_uncommitted_libraries(library_key, True, False)
commit_library_and_verify(library_key)

View File

@@ -113,8 +113,7 @@ oauthlib # OAuth specification support for authentica
olxcleaner
openedx-calc # Library supporting mathematical calculations for Open edX
openedx-django-require
# openedx-events 3.1.0 introduces producer API
openedx-events>=8.2.0 # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-events>=8.3.0 # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)
openedx-mongodbproxy
openedx-django-wiki

View File

@@ -12,8 +12,14 @@ from contextlib import contextmanager
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from openedx_events.content_authoring.data import XBlockData
from openedx_events.content_authoring.signals import XBLOCK_DELETED, XBLOCK_PUBLISHED
from openedx_events.content_authoring.data import CourseData, XBlockData
from openedx_events.content_authoring.signals import (
COURSE_CREATED,
XBLOCK_CREATED,
XBLOCK_DELETED,
XBLOCK_PUBLISHED,
XBLOCK_UPDATED
)
from django.utils.timezone import datetime, timezone
from xmodule.assetstore import AssetMetadata
@@ -127,6 +133,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
"""
ModuleStore knows how to route requests to the right persistence ms
"""
def __init__(
self,
contentstore,
@@ -670,6 +677,14 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
# add new course to the mapping
self.mappings[course_key] = store
# .. event_implemented_name: COURSE_CREATED
COURSE_CREATED.send_event(
time=datetime.now(timezone.utc),
course=CourseData(
course_key=course_key,
)
)
return course
@strip_key
@@ -742,7 +757,17 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
in the newly created block
"""
modulestore = self._verify_modulestore_support(course_key, 'create_item')
return modulestore.create_item(user_id, course_key, block_type, block_id=block_id, fields=fields, **kwargs)
xblock = modulestore.create_item(user_id, course_key, block_type, block_id=block_id, fields=fields, **kwargs)
# .. event_implemented_name: XBLOCK_CREATED
XBLOCK_CREATED.send_event(
time=datetime.now(timezone.utc),
xblock_info=XBlockData(
usage_key=xblock.location.for_branch(None),
block_type=block_type,
version=xblock.location
)
)
return xblock
@strip_key
@prepare_asides
@@ -763,7 +788,19 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
in the newly created block
"""
modulestore = self._verify_modulestore_support(parent_usage_key.course_key, 'create_child')
return modulestore.create_child(user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs) # lint-amnesty, pylint: disable=line-too-long
xblock = modulestore.create_child(
user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs
)
# .. event_implemented_name: XBLOCK_CREATED
XBLOCK_CREATED.send_event(
time=datetime.now(timezone.utc),
xblock_info=XBlockData(
usage_key=xblock.location.for_branch(None),
block_type=block_type,
version=xblock.location
)
)
return xblock
@strip_key
@prepare_asides
@@ -792,7 +829,17 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
(content, children, and metadata) attribute the change to the given user.
"""
store = self._verify_modulestore_support(xblock.location.course_key, 'update_item')
return store.update_item(xblock, user_id, allow_not_found, **kwargs)
xblock = store.update_item(xblock, user_id, allow_not_found, **kwargs)
# .. event_implemented_name: XBLOCK_UPDATED
XBLOCK_UPDATED.send_event(
time=datetime.now(timezone.utc),
xblock_info=XBlockData(
usage_key=xblock.location.for_branch(None),
block_type=xblock.location.block_type,
version=xblock.location
)
)
return xblock
@strip_key
def delete_item(self, location, user_id, **kwargs): # lint-amnesty, pylint: disable=arguments-differ

View File

@@ -15,8 +15,14 @@ from uuid import uuid4
from unittest.mock import Mock, call, patch
import ddt
from openedx_events.content_authoring.data import XBlockData
from openedx_events.content_authoring.signals import XBLOCK_DELETED, XBLOCK_PUBLISHED
from openedx_events.content_authoring.data import CourseData, XBlockData
from openedx_events.content_authoring.signals import (
COURSE_CREATED,
XBLOCK_CREATED,
XBLOCK_DELETED,
XBLOCK_PUBLISHED,
XBLOCK_UPDATED
)
from openedx_events.tests.utils import OpenEdxEventsTestMixin
import pymongo
import pytest
@@ -113,6 +119,9 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest, OpenEdxEventsTestMixin):
'xblock_mixins': modulestore_options['xblock_mixins'],
}
ENABLED_OPENEDX_EVENTS = [
"org.openedx.content_authoring.course.created.v1",
"org.openedx.content_authoring.xblock.created.v1",
"org.openedx.content_authoring.xblock.updated.v1",
"org.openedx.content_authoring.xblock.deleted.v1",
"org.openedx.content_authoring.xblock.published.v1",
]
@@ -498,7 +507,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
assert {'course', 'about'}.issubset({item.location.block_type for item in items}) # pylint: disable=line-too-long
# Assert that about is a detached category found in get_items
assert [item.location.block_type for item in items if item.location.block_type == 'about'][0]\
in DETACHED_XBLOCK_TYPES
in DETACHED_XBLOCK_TYPES
assert len(items) == 2
# Check that orphans are not found
@@ -741,6 +750,79 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
self.store.delete_item(vertical.location, self.user_id)
assert not self._has_changes(sequential.location)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_course_create_event(self, default_ms):
"""
Check that COURSE_CREATED event is sent when a course is created.
"""
self.initdb(default_ms)
event_receiver = Mock()
COURSE_CREATED.connect(event_receiver)
test_course = self.store.create_course('test_org', 'test_course', 'test_run', self.user_id)
event_receiver.assert_called()
self.assertDictContainsSubset(
{
"signal": COURSE_CREATED,
"sender": None,
"course": CourseData(
course_key=test_course.id,
),
},
event_receiver.call_args.kwargs
)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_xblock_create_event(self, default_ms):
"""
Check that XBLOCK_CREATED event is sent when xblock is created.
"""
self.initdb(default_ms)
event_receiver = Mock()
XBLOCK_CREATED.connect(event_receiver)
test_course = self.store.create_course('test_org', 'test_course', 'test_run', self.user_id)
# create sequential to test against
sequential = self.store.create_child(self.user_id, test_course.location, 'sequential', 'test_sequential')
event_receiver.assert_called()
assert event_receiver.call_args.kwargs['signal'] == XBLOCK_CREATED
assert event_receiver.call_args.kwargs['xblock_info'].usage_key == sequential.location
assert event_receiver.call_args.kwargs['xblock_info'].block_type == sequential.location.block_type
assert event_receiver.call_args.kwargs['xblock_info'].version.for_branch(None) == sequential.location
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_xblock_update_event(self, default_ms):
"""
Check that XBLOCK_UPDATED event is sent when xblock is updated.
"""
self.initdb(default_ms)
event_receiver = Mock()
XBLOCK_UPDATED.connect(event_receiver)
test_course = self.store.create_course('test_org', 'test_course', 'test_run', self.user_id)
# create sequential to test against
sequential = self.store.create_child(self.user_id, test_course.location, 'sequential', 'test_sequential')
# Change the xblock
sequential.display_name = 'Updated Display Name'
self.store.update_item(sequential, user_id=self.user_id)
event_receiver.assert_called()
assert event_receiver.call_args.kwargs['signal'] == XBLOCK_UPDATED
assert event_receiver.call_args.kwargs['xblock_info'].usage_key == sequential.location
assert event_receiver.call_args.kwargs['xblock_info'].block_type == sequential.location.block_type
assert event_receiver.call_args.kwargs['xblock_info'].version.for_branch(None) == sequential.location
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_xblock_publish_event(self, default_ms):
"""
@@ -1217,7 +1299,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
"""
for child_location, parent_location, revision in expected_results:
assert parent_location.for_branch(None) if parent_location else parent_location == \
self.store.get_parent_location(child_location, revision=revision)
self.store.get_parent_location(child_location, revision=revision)
def verify_item_parent(self, item_location, expected_parent_location, old_parent_location, is_reverted=False):
"""
@@ -2461,7 +2543,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
Asserts the number of problems with the given display name is the given expected number.
"""
assert len(self.store.get_items(course_key.for_branch(None), settings={'display_name': display_name})) ==\
expected_number
expected_number
def assertProblemNameEquals(expected_display_name):
"""
@@ -3085,6 +3167,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
Tests which publish (or don't publish) items - and then export/import the course,
checking the state of the imported items.
"""
def setUp(self):
"""
Set up the database for testing
@@ -3690,6 +3773,7 @@ class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
"""
Tests of the MixedModulestore interface methods with XBlock asides.
"""
def setUp(self):
"""
Setup environment for testing