Skip to content

Commit

Permalink
Merge pull request #7 from eduNEXT/MJG/events_tooling
Browse files Browse the repository at this point in the history
feat: add necessary tooling for Open edX events
  • Loading branch information
mariajgrimaldi authored Jul 28, 2021
2 parents 0f11b1f + 4ed2f5e commit 8f1ad8c
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Change Log
Unreleased
~~~~~~~~~~
Added
_____
* Add tooling needed to create and trigger events in Open edX platform


[0.2.0] - 2021-07-28
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
62 changes: 62 additions & 0 deletions openedx_events/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Data attributes for events within the architecture subdomain `learning`.
These attributes follow the form of attr objects specified in OEP-49 data
pattern.
"""
import socket
from datetime import datetime
from uuid import UUID, uuid1

import attr
from django.conf import settings

import openedx_events


@attr.s(frozen=True)
class EventsMetadata:
"""
Attributes defined for Open edX Events metadata object.
The attributes defined in this class are a subset of the
OEP-41: Asynchronous Server Event Message Format.
Arguments:
id (UUID): event identifier.
event_type (str): name of the event.
minorversion (int): version of the event type.
source (str): logical source of an event.
sourcehost (str): physical source of the event.
time (datetime): timestamp when the event was sent.
sourcelib (str): Open edX Events library version.
"""

id = attr.ib(type=UUID, init=False)
event_type = attr.ib(type=str)
minorversion = attr.ib(type=int, converter=attr.converters.default_if_none(0))
source = attr.ib(type=str, init=False)
sourcehost = attr.ib(type=str, init=False)
time = attr.ib(type=datetime, init=False)
sourcelib = attr.ib(type=tuple, init=False)

def __attrs_post_init__(self):
"""
Post-init hook that generates metadata for the Open edX Event.
"""
# Have to use this to get around the fact that the class is frozen
# (which we almost always want, but not while we're initializing it).
# Taken from edX Learning Sequences data file.
object.__setattr__(self, "id", uuid1())
object.__setattr__(
self,
"source",
"openedx/{service}/web".format(
service=getattr(settings, "SERVICE_VARIANT", "")
),
)
object.__setattr__(self, "sourcehost", socket.gethostname())
object.__setattr__(self, "time", datetime.utcnow())
object.__setattr__(
self, "sourcelib", tuple(map(int, openedx_events.__version__.split(".")))
)
69 changes: 69 additions & 0 deletions openedx_events/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
Custom exceptions thrown by Open edX events tooling.
"""


class OpenEdxEventException(Exception):
"""
Base class for Open edX Events exceptions.
"""

def __init__(self, message=""):
"""
Init method for OpenEdxEventException base class.
Arguments:
message (str): message describing why the exception was raised.
"""
super().__init__()
self.message = message

def __str__(self):
"""
Show string representation of OpenEdxEventException using its message.
"""
return self.message


class InstantiationError(OpenEdxEventException):
"""
Describes errors that occur while instantiating events.
This exception is raised when there's an error instantiating an Open edX
event, it can be that a required argument for the event definition is
missing.
"""

def __init__(self, event_type="", message=""):
"""
Init method for InstantiationError custom exception class.
Arguments:
event_type (str): name of the event raising the exception.
message (str): message describing why the exception was raised.
"""
super().__init__(
message="InstantiationError {event_type}: {message}".format(
event_type=event_type, message=message
)
)


class SenderValidationError(OpenEdxEventException):
"""
Describes errors that occur while validating arguments of send methods.
"""

def __init__(self, event_type="", message=""):
"""
Init method for SenderValidationError custom exception class.
Arguments:
event_type (str): name of the event raising the exception.
message (str): message describing why the exception was raised.
"""
super().__init__(
message="SenderValidationError {event_type}: {message}".format(
event_type=event_type, message=message
)
)
193 changes: 193 additions & 0 deletions openedx_events/tests/test_tooling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
"""This file contains all test for the tooling.py file.
Classes:
EventsToolingTest: Test events tooling.
"""
from unittest.mock import Mock, patch

import attr
import ddt
from django.test import TestCase, override_settings

from openedx_events.exceptions import InstantiationError, SenderValidationError
from openedx_events.tooling import OpenEdxPublicSignal


@ddt.ddt
class OpenEdxPublicSignalTest(TestCase):
"""
Test cases for Open edX events base class.
"""

def setUp(self):
"""
Setup common conditions for every test case.
"""
super().setUp()
self.event_type = "org.openedx.learning.session.login.completed.v1"
self.user_mock = Mock()
self.data_attr = {
"user": Mock,
}
self.public_signal = OpenEdxPublicSignal(
event_type=self.event_type,
data=self.data_attr,
)

def test_string_representation(self):
"""
This methods checks the string representation for events base class.
Expected behavior:
The representation contains the event_type.
"""
self.assertIn(self.event_type, str(self.public_signal))

@override_settings(SERVICE_VARIANT="lms")
@patch("openedx_events.data.openedx_events")
@patch("openedx_events.data.socket")
def test_get_signal_metadata(self, socket_mock, events_package_mock):
"""
This methods tests getting the generated metadata for an event.
Expected behavior:
Returns the metadata containing information about the event.
"""
events_package_mock.__version__ = "0.1.0"
socket_mock.gethostname.return_value = "edx.devstack.lms"
expected_metadata = {
"event_type": self.event_type,
"minorversion": 0,
"source": "openedx/lms/web",
"sourcehost": "edx.devstack.lms",
"sourcelib": [0, 1, 0],
}

metadata = self.public_signal.generate_signal_metadata()

self.assertDictContainsSubset(expected_metadata, attr.asdict(metadata))

@ddt.data(
("", {"user": Mock()}, "event_type"),
("org.openedx.learning.session.login.completed.v1", None, "data"),
)
@ddt.unpack
def test_event_instantiation_exception(
self, event_type, event_data, missing_argument
):
"""
This method tests when an event is instantiated without event_type or
event data.
Expected behavior:
An InstantiationError exception is raised.
"""
exception_message = "InstantiationError {event_type}: Missing required argument '{missing_argument}'".format(
event_type=event_type, missing_argument=missing_argument
)

with self.assertRaisesMessage(InstantiationError, exception_message):
OpenEdxPublicSignal(event_type=event_type, data=event_data)

@patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata")
@patch("openedx_events.tooling.Signal.send")
def test_send_event_successfully(self, send_mock, fake_metadata):
"""
This method tests the process of sending an event.
Expected behavior:
The event is sent as a django signal.
"""
expected_metadata = {
"some_data": "data",
"raise_exception": True,
}
fake_metadata.return_value = expected_metadata

self.public_signal.send_event(user=self.user_mock)

send_mock.assert_called_once_with(
sender=None,
user=self.user_mock,
metadata=expected_metadata,
)

@patch("openedx_events.tooling.OpenEdxPublicSignal.generate_signal_metadata")
@patch("openedx_events.tooling.Signal.send_robust")
def test_send_robust_event_successfully(self, send_robust_mock, fake_metadata):
"""
This method tests the process of sending an event.
Expected behavior:
The event is sent as a django signal.
"""
expected_metadata = {
"some_data": "data",
"raise_exception": True,
}
fake_metadata.return_value = expected_metadata

self.public_signal.send_event(user=self.user_mock, send_robust=True)

send_robust_mock.assert_called_once_with(
sender=None,
user=self.user_mock,
metadata=expected_metadata,
)

@ddt.data(
(
{"student": Mock()},
"SenderValidationError org.openedx.learning.session.login.completed.v1: "
"Missing required argument 'user'",
),
(
{"user": {"student": Mock()}},
"SenderValidationError org.openedx.learning.session.login.completed.v1: "
"The argument 'user' is not instance of the Class Attribute 'type'",
),
(
{"student": Mock(), "user": Mock()},
"SenderValidationError org.openedx.learning.session.login.completed.v1: "
"There's a mismatch between initialization data and send_event arguments",
),
)
@ddt.unpack
def test_invalid_sender(self, send_arguments, exception_message):
"""
This method tests sending an event with invalid setup on the sender
side.
Expected behavior:
A SenderValidationError exception is raised.
"""
with self.assertRaisesMessage(SenderValidationError, exception_message):
self.public_signal.send_event(**send_arguments)

def test_send_event_with_django(self):
"""
This method tests sending an event using the `send` built-in Django
method.
Expected behavior:
A warning is showed advicing to use Open edX events custom
send_signal method.
"""
message = "Please, use 'send_event' when triggering an Open edX event."

with self.assertWarns(Warning, msg=message):
self.public_signal.send(sender=Mock())

def test_send_robust_event_with_django(self):
"""
This method tests sending an event using the `send` built-in Django
method.
Expected behavior:
A warning is showed advicing to use Open edX events custom
send_signal method.
"""
message = "Please, use 'send_event' with send_robust equals to True when triggering an Open edX event."

with self.assertWarns(Warning, msg=message):
self.public_signal.send_robust(sender=Mock())
Loading

0 comments on commit 8f1ad8c

Please sign in to comment.