feat: hook xblock publish, delete and duplicate openedx-events (#31350)
This commit is contained in:
@@ -13,7 +13,12 @@ from django.dispatch import receiver
|
||||
from edx_toggles.toggles import SettingToggle
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx_events.content_authoring.data import CourseCatalogData, CourseScheduleData
|
||||
from openedx_events.content_authoring.signals import COURSE_CATALOG_INFO_CHANGED
|
||||
from openedx_events.content_authoring.signals import (
|
||||
COURSE_CATALOG_INFO_CHANGED,
|
||||
XBLOCK_DELETED,
|
||||
XBLOCK_DUPLICATED,
|
||||
XBLOCK_PUBLISHED,
|
||||
)
|
||||
from openedx_events.event_bus import get_producer
|
||||
from pytz import UTC
|
||||
|
||||
@@ -166,6 +171,42 @@ def listen_for_course_catalog_info_changed(sender, signal, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
@receiver(XBLOCK_PUBLISHED)
|
||||
def listen_for_xblock_published(sender, signal, **kwargs):
|
||||
"""
|
||||
Publish XBLOCK_PUBLISHED signals onto the event bus.
|
||||
"""
|
||||
get_producer().send(
|
||||
signal=XBLOCK_PUBLISHED, topic='xblock-published',
|
||||
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
|
||||
event_metadata=kwargs['metadata'],
|
||||
)
|
||||
|
||||
|
||||
@receiver(XBLOCK_DELETED)
|
||||
def listen_for_xblock_deleted(sender, signal, **kwargs):
|
||||
"""
|
||||
Publish XBLOCK_DELETED signals onto the event bus.
|
||||
"""
|
||||
get_producer().send(
|
||||
signal=XBLOCK_DELETED, topic='xblock-deleted',
|
||||
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
|
||||
event_metadata=kwargs['metadata'],
|
||||
)
|
||||
|
||||
|
||||
@receiver(XBLOCK_DUPLICATED)
|
||||
def listen_for_xblock_duplicated(sender, signal, **kwargs):
|
||||
"""
|
||||
Publish XBLOCK_DUPLICATED signals onto the event bus.
|
||||
"""
|
||||
get_producer().send(
|
||||
signal=XBLOCK_DUPLICATED, topic='xblock-duplicated',
|
||||
event_key_field='xblock_info.usage_key', event_data={'xblock_info': kwargs['xblock_info']},
|
||||
event_metadata=kwargs['metadata'],
|
||||
)
|
||||
|
||||
|
||||
@receiver(SignalHandler.course_deleted)
|
||||
def listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
|
||||
@@ -11,9 +11,12 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.utils.timezone import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from edx_django_utils.plugins import pluggable_override
|
||||
from openedx_events.content_authoring.data import DuplicatedXBlockData
|
||||
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
|
||||
from edx_proctoring.api import (
|
||||
does_backend_support_onboarding,
|
||||
get_exam_by_content_id,
|
||||
@@ -959,6 +962,16 @@ def _duplicate_block(parent_usage_key, duplicate_source_usage_key, user, display
|
||||
parent.children.append(dest_block.location)
|
||||
store.update_item(parent, user.id)
|
||||
|
||||
# .. event_implemented_name: XBLOCK_DUPLICATED
|
||||
XBLOCK_DUPLICATED.send_event(
|
||||
time=datetime.now(timezone.utc),
|
||||
xblock_info=DuplicatedXBlockData(
|
||||
usage_key=dest_block.location,
|
||||
block_type=dest_block.location.block_type,
|
||||
source_usage_key=duplicate_source_usage_key,
|
||||
)
|
||||
)
|
||||
|
||||
return dest_block.location
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ from django.http import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from openedx_events.content_authoring.data import DuplicatedXBlockData
|
||||
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin
|
||||
from edx_proctoring.exceptions import ProctoredExamNotFoundException
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.asides import AsideUsageKeyV2
|
||||
@@ -550,6 +553,7 @@ class DuplicateHelper:
|
||||
self._check_equality(source_usage_key, usage_key, parent_usage_key, check_asides=check_asides),
|
||||
"Duplicated item differs from original"
|
||||
)
|
||||
return usage_key
|
||||
|
||||
def _check_equality(self, source_usage_key, duplicate_usage_key, parent_usage_key=None, check_asides=False,
|
||||
is_child=False):
|
||||
@@ -642,11 +646,25 @@ class DuplicateHelper:
|
||||
return self.response_usage_key(resp)
|
||||
|
||||
|
||||
class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Test the duplicate method.
|
||||
"""
|
||||
|
||||
ENABLED_OPENEDX_EVENTS = [
|
||||
"org.openedx.content_authoring.xblock.duplicated.v1",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
""" Creates the test course structure and a few components to 'duplicate'. """
|
||||
super().setUp()
|
||||
@@ -684,6 +702,27 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
|
||||
self._duplicate_and_verify(self.seq_usage_key, self.chapter_usage_key)
|
||||
self._duplicate_and_verify(self.chapter_usage_key, self.usage_key)
|
||||
|
||||
def test_duplicate_event(self):
|
||||
"""
|
||||
Check that XBLOCK_DUPLICATED event is sent when xblock is duplicated.
|
||||
"""
|
||||
event_receiver = Mock()
|
||||
XBLOCK_DUPLICATED.connect(event_receiver)
|
||||
usage_key = self._duplicate_and_verify(self.vert_usage_key, self.seq_usage_key)
|
||||
event_receiver.assert_called()
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": XBLOCK_DUPLICATED,
|
||||
"sender": None,
|
||||
"xblock_info": DuplicatedXBlockData(
|
||||
usage_key=usage_key,
|
||||
block_type=usage_key.block_type,
|
||||
source_usage_key=self.vert_usage_key,
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
def test_ordering(self):
|
||||
"""
|
||||
Tests the a duplicated xblock appears immediately after its source
|
||||
|
||||
@@ -195,3 +195,15 @@ Content Authoring Events
|
||||
* - `COURSE_CATALOG_INFO_CHANGED <https://github.com/openedx/openedx-events/blob/main/openedx_events/content_authoring/signals.py#L23>`_
|
||||
- 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>`_
|
||||
- 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>`_
|
||||
- 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>`_
|
||||
- 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>`_
|
||||
|
||||
@@ -12,7 +12,10 @@ 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 django.utils.timezone import datetime, timezone
|
||||
from xmodule.assetstore import AssetMetadata
|
||||
|
||||
from . import XMODULE_FIELDS_WITH_USAGE_KEYS, ModuleStoreWriteBase
|
||||
@@ -797,7 +800,16 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
|
||||
Delete the given item from persistence. kwargs allow modulestore specific parameters.
|
||||
"""
|
||||
store = self._verify_modulestore_support(location.course_key, 'delete_item')
|
||||
return store.delete_item(location, user_id=user_id, **kwargs)
|
||||
item = store.delete_item(location, user_id=user_id, **kwargs)
|
||||
# .. event_implemented_name: XBLOCK_DELETED
|
||||
XBLOCK_DELETED.send_event(
|
||||
time=datetime.now(timezone.utc),
|
||||
xblock_info=XBlockData(
|
||||
usage_key=location,
|
||||
block_type=location.block_type,
|
||||
)
|
||||
)
|
||||
return item
|
||||
|
||||
def revert_to_published(self, location, user_id):
|
||||
"""
|
||||
@@ -911,7 +923,16 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
|
||||
Returns the newly published item.
|
||||
"""
|
||||
store = self._verify_modulestore_support(location.course_key, 'publish')
|
||||
return store.publish(location, user_id, **kwargs)
|
||||
item = store.publish(location, user_id, **kwargs)
|
||||
# .. event_implemented_name: XBLOCK_PUBLISHED
|
||||
XBLOCK_PUBLISHED.send_event(
|
||||
time=datetime.now(timezone.utc),
|
||||
xblock_info=XBlockData(
|
||||
usage_key=location,
|
||||
block_type=location.block_type,
|
||||
)
|
||||
)
|
||||
return item
|
||||
|
||||
@strip_key
|
||||
def unpublish(self, location, user_id, **kwargs):
|
||||
|
||||
@@ -15,6 +15,9 @@ 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.tests.utils import OpenEdxEventsTestMixin
|
||||
import pymongo
|
||||
import pytest
|
||||
# Mixed modulestore depends on django, so we'll manually configure some django settings
|
||||
@@ -63,7 +66,7 @@ if not settings.configured:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommonMixedModuleStoreSetup(CourseComparisonTest):
|
||||
class CommonMixedModuleStoreSetup(CourseComparisonTest, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and
|
||||
Location-based dbs)
|
||||
@@ -109,6 +112,20 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest):
|
||||
],
|
||||
'xblock_mixins': modulestore_options['xblock_mixins'],
|
||||
}
|
||||
ENABLED_OPENEDX_EVENTS = [
|
||||
"org.openedx.content_authoring.xblock.deleted.v1",
|
||||
"org.openedx.content_authoring.xblock.published.v1",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up class method for the Test class.
|
||||
This method starts manually events isolation. Explanation here:
|
||||
openedx/core/djangoapps/user_authn/views/tests/test_events.py#L44
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -724,6 +741,71 @@ 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_xblock_publish_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_PUBLISHED event is sent when xblock is published.
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
event_receiver = Mock()
|
||||
XBLOCK_PUBLISHED.connect(event_receiver)
|
||||
|
||||
test_course = self.store.create_course('test_org', 'test_course', 'test_run', self.user_id)
|
||||
|
||||
# create sequential and vertical to test against
|
||||
sequential = self.store.create_child(self.user_id, test_course.location, 'sequential', 'test_sequential')
|
||||
self.store.create_child(self.user_id, sequential.location, 'vertical', 'test_vertical')
|
||||
|
||||
# publish sequential changes
|
||||
self.store.publish(sequential.location, self.user_id)
|
||||
|
||||
event_receiver.assert_called()
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": XBLOCK_PUBLISHED,
|
||||
"sender": None,
|
||||
"xblock_info": XBlockData(
|
||||
usage_key=sequential.location,
|
||||
block_type=sequential.location.block_type,
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_xblock_delete_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_DELETED event is sent when xblock is deleted.
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
event_receiver = Mock()
|
||||
XBLOCK_DELETED.connect(event_receiver)
|
||||
|
||||
test_course = self.store.create_course('test_org', 'test_course', 'test_run', self.user_id)
|
||||
|
||||
# create sequential and vertical to test against
|
||||
sequential = self.store.create_child(self.user_id, test_course.location, 'sequential', 'test_sequential')
|
||||
vertical = self.store.create_child(self.user_id, sequential.location, 'vertical', 'test_vertical')
|
||||
|
||||
# publish sequential changes
|
||||
self.store.publish(sequential.location, self.user_id)
|
||||
|
||||
# delete vertical
|
||||
self.store.delete_item(vertical.location, self.user_id)
|
||||
|
||||
event_receiver.assert_called()
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": XBLOCK_DELETED,
|
||||
"sender": None,
|
||||
"xblock_info": XBlockData(
|
||||
usage_key=vertical.location,
|
||||
block_type=vertical.location.block_type,
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
def setup_has_changes(self, default_ms):
|
||||
"""
|
||||
Common set up for has_changes tests below.
|
||||
|
||||
Reference in New Issue
Block a user