Skip to content

Commit

Permalink
feat: hook xblock publish, delete and duplicate openedx-events (#31350)
Browse files Browse the repository at this point in the history
  • Loading branch information
navinkarkera committed Feb 14, 2023
1 parent 8cfcc04 commit 4697adc
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 5 deletions.
43 changes: 42 additions & 1 deletion cms/djangoapps/contentstore/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
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

Expand Down Expand Up @@ -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
"""
Expand Down
13 changes: 13 additions & 0 deletions cms/djangoapps/contentstore/views/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
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,
Expand Down Expand Up @@ -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


Expand Down
41 changes: 40 additions & 1 deletion cms/djangoapps/contentstore/views/tests/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
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
Expand Down Expand Up @@ -550,6 +553,7 @@ def _duplicate_and_verify(self, source_usage_key, parent_usage_key, check_asides
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):
Expand Down Expand Up @@ -642,11 +646,25 @@ def _duplicate_item(self, parent_usage_key, source_usage_key, display_name=None)
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()
Expand Down Expand Up @@ -684,6 +702,27 @@ def test_duplicate_equality(self):
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
Expand Down
12 changes: 12 additions & 0 deletions docs/guides/hooks/events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_
25 changes: 23 additions & 2 deletions xmodule/modulestore/mixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
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
Expand Down Expand Up @@ -797,7 +800,16 @@ def delete_item(self, location, user_id, **kwargs): # lint-amnesty, pylint: dis
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):
"""
Expand Down Expand Up @@ -911,7 +923,16 @@ def publish(self, location, user_id, **kwargs):
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):
Expand Down
84 changes: 83 additions & 1 deletion xmodule/modulestore/tests/test_mixed_modulestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
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
Expand Down Expand Up @@ -63,7 +66,7 @@
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)
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -724,6 +741,71 @@ def test_publish_automatically_after_delete_unit(self, default_ms):
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.
Expand Down

0 comments on commit 4697adc

Please sign in to comment.