Skip to content

Commit

Permalink
feat: adds events to handle changes in xblocks (#143)
Browse files Browse the repository at this point in the history
Adds events and data classes to allow consumers to react on xblock events like published, deleted and duplicated.
The idea is to fire these events in delete_item, publish_item and _duplicate_item and update skills related to the xblock in taxonomy-connector (part of course-discovery).
  • Loading branch information
navinkarkera authored Jan 3, 2023
1 parent 10da4d4 commit 75216de
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 8 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Change Log
Unreleased
----------

[4.1.0] - 2023-01-03
---------------------
Added
~~~~~~~
* Added new XBLOCK_PUBLISHED, XBLOCK_DUPLICATED and XBLOCK_DELETED signals in content_authoring.
* Added XBlockData and DuplicatedXBlockData classes
* Added custom UsageKeyAvroSerializer for opaque_keys UsageKey.

[4.0.0] - 2022-12-01
--------------------
Changed
Expand Down
2 changes: 1 addition & 1 deletion openedx_events/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
more information about the project.
"""

__version__ = "4.0.0"
__version__ = "4.1.0"
30 changes: 29 additions & 1 deletion openedx_events/content_authoring/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from datetime import datetime

import attr
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey


@attr.s(frozen=True)
Expand Down Expand Up @@ -54,3 +54,31 @@ class CourseCatalogData:
schedule_data = attr.ib(type=CourseScheduleData)
hidden = attr.ib(type=bool, default=False)
invitation_only = attr.ib(type=bool, default=False)


@attr.s(frozen=True)
class XBlockData:
"""
Data about changed XBlock.
Arguments:
usage_key (UsageKey): identifier of the XBlock object.
block_type (str): type of block.
"""

usage_key = attr.ib(type=UsageKey)
block_type = attr.ib(type=str)


@attr.s(frozen=True)
class DuplicatedXBlockData(XBlockData):
"""
Data about duplicated XBlock.
This class extends XBlockData to include source_usage_key.
Arguments:
source_usage_key (UsageKey): identifier of the source XBlock object.
"""

source_usage_key = attr.ib(type=UsageKey)
43 changes: 42 additions & 1 deletion openedx_events/content_authoring/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
docs/decisions/0003-events-payload.rst
"""

from openedx_events.content_authoring.data import CourseCatalogData
from openedx_events.content_authoring.data import CourseCatalogData, DuplicatedXBlockData, XBlockData
from openedx_events.tooling import OpenEdxPublicSignal

# .. event_type: org.openedx.content_authoring.course.catalog_info.changed.v1
Expand All @@ -21,3 +21,44 @@
"catalog_info": CourseCatalogData,
}
)


# .. event_type: org.openedx.content_authoring.xblock.published.v1
# .. event_name: XBLOCK_PUBLISHED
# .. event_description: Fired when an XBlock is published. If a parent block
# with changes in one or more child blocks is published, only a single
# XBLOCK_PUBLISHED event is fired with parent block details.
# For example: If a section is published with changes in multiple units,
# only a single event is fired with section details like :
# `XBlockData(usage_key="section-usage-key", block_type="chapter")`
# .. event_data: XBlockData
XBLOCK_PUBLISHED = OpenEdxPublicSignal(
event_type="org.openedx.content_authoring.xblock.published.v1",
data={
"xblock_info": XBlockData,
}
)


# .. event_type: org.openedx.content_authoring.xblock.deleted.v1
# .. event_name: XBLOCK_DELETED
# .. event_description: Fired when an XBlock is deleted.
# .. event_data: XBlockData
XBLOCK_DELETED = OpenEdxPublicSignal(
event_type="org.openedx.content_authoring.xblock.deleted.v1",
data={
"xblock_info": XBlockData,
}
)


# .. event_type: org.openedx.content_authoring.xblock.duplicated.v1
# .. event_name: XBLOCK_DUPLICATED
# .. event_description: Fired when an XBlock is duplicated in Studio.
# .. event_data: DuplicatedXBlockData
XBLOCK_DUPLICATED = OpenEdxPublicSignal(
event_type="org.openedx.content_authoring.xblock.duplicated.v1",
data={
"xblock_info": DuplicatedXBlockData,
}
)
23 changes: 21 additions & 2 deletions openedx_events/event_bus/avro/custom_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from abc import ABC, abstractmethod
from datetime import datetime

from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey

from openedx_events.event_bus.avro.types import PYTHON_TYPE_TO_AVRO_MAPPING

Expand Down Expand Up @@ -71,4 +71,23 @@ def deserialize(data: str):
return datetime.fromisoformat(data)


DEFAULT_CUSTOM_SERIALIZERS = [CourseKeyAvroSerializer, DatetimeAvroSerializer]
class UsageKeyAvroSerializer(BaseCustomTypeAvroSerializer):
"""
CustomTypeAvroSerializer for UsageKey class.
"""

cls = UsageKey
field_type = PYTHON_TYPE_TO_AVRO_MAPPING[str]

@staticmethod
def serialize(obj) -> str:
"""Serialize obj into string."""
return str(obj)

@staticmethod
def deserialize(data: str):
"""Deserialize string into obj."""
return UsageKey.from_string(data)


DEFAULT_CUSTOM_SERIALIZERS = [CourseKeyAvroSerializer, DatetimeAvroSerializer, UsageKeyAvroSerializer]
5 changes: 4 additions & 1 deletion openedx_events/event_bus/avro/tests/test_avro.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime
from unittest import TestCase

from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey

# Each new folder with signals must be manually imported in order for the signals to be cached
# and used in the unit tests. Using 'disable=reimported' with pylint will work,
Expand Down Expand Up @@ -49,6 +49,9 @@ def generate_test_event_data_for_data_type(data_type):
str: "default",
float: 1.0,
CourseKey: CourseKey.from_string("course-v1:edX+DemoX.1+2014"),
UsageKey: UsageKey.from_string(
"block-v1:edx+DemoX+Demo_course+type@video+block@UaEBjyMjcLW65gaTXggB93WmvoxGAJa0JeHRrDThk",
),
datetime: datetime.now(),
}
for attribute in data_type.__attrs_attrs__:
Expand Down
17 changes: 16 additions & 1 deletion openedx_events/event_bus/avro/tests/test_deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from datetime import datetime
from unittest import TestCase

from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey

from openedx_events.event_bus.avro.deserializer import AvroSignalDeserializer
from openedx_events.event_bus.avro.tests.test_utilities import (
Expand Down Expand Up @@ -102,6 +102,21 @@ def test_default_coursekey_deserialization(self):
self.assertIsInstance(course_deserialized, CourseKey)
self.assertEqual(course_deserialized, course_key)

def test_default_usagekey_deserialization(self):
"""
Test deserialization of UsageKey
"""
SIGNAL = create_simple_signal({"usage_key": UsageKey})
deserializer = AvroSignalDeserializer(SIGNAL)
usage_key = UsageKey.from_string(
"block-v1:edx+DemoX+Demo_course+type@video+block@UaEBjyMjcLW65gaTXggB93WmvoxGAJa0JeHRrDThk",
)
as_dict = {"usage_key": str(usage_key)}
event_data = deserializer.from_dict(as_dict)
usage_key_deserialized = event_data["usage_key"]
self.assertIsInstance(usage_key_deserialized, UsageKey)
self.assertEqual(usage_key_deserialized, usage_key)

def test_deserialization_with_custom_serializer(self):
SIGNAL = create_simple_signal({"test_data": NonAttrs})
deserializer = SpecialDeserializer(SIGNAL)
Expand Down
15 changes: 14 additions & 1 deletion openedx_events/event_bus/avro/tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.keys import CourseKey, UsageKey

from openedx_events.event_bus.avro.serializer import AvroSignalSerializer
from openedx_events.event_bus.avro.tests.test_utilities import (
Expand Down Expand Up @@ -105,6 +105,19 @@ def test_default_coursekey_serialization(self):
data_dict = serializer.to_dict(test_data)
self.assertDictEqual(data_dict, {"course": str(course_key)})

def test_default_usagekey_serialization(self):
"""
Test serialization of UsageKey
"""
SIGNAL = create_simple_signal({"usage_key": UsageKey})
serializer = AvroSignalSerializer(SIGNAL)
usage_key = UsageKey.from_string(
"block-v1:edx+DemoX+Demo_course+type@video+block@UaEBjyMjcLW65gaTXggB93WmvoxGAJa0JeHRrDThk",
)
test_data = {"usage_key": usage_key}
data_dict = serializer.to_dict(test_data)
self.assertDictEqual(data_dict, {"usage_key": str(usage_key)})

def test_serialization_with_custom_serializer(self):
SIGNAL = create_simple_signal({"test_data": NonAttrs})

Expand Down

0 comments on commit 75216de

Please sign in to comment.