Skip to content

Commit

Permalink
Merge pull request #370 from nelc/and/add_openedx_filters
Browse files Browse the repository at this point in the history
feat: implement XAPI openedx filters
  • Loading branch information
Ian2012 authored Nov 20, 2023
2 parents 202ca1b + e9fd098 commit 65ddeae
Show file tree
Hide file tree
Showing 27 changed files with 288 additions and 23 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Change Log
Unreleased
~~~~~~~~~~

[7.1.0]
~~~~~~~

* Add support for openedx-filter that basically allows to extend or change the standard behavior

[7.0.2]
~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions docs/_static/theme_overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
.wy-table-responsive {
overflow: visible !important;
}

.bd-page-width {
max-width: 96rem;
}
4 changes: 4 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ def get_version(*file_paths):
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

html_css_files = [
'theme_overrides.css',
]

# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
Expand Down
44 changes: 44 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,47 @@ A sample override for ``xapi`` backend is presented below. Here we are allowing
}
}
OpenEdx Filters
===============
This is an integration that allows to modify current standard outputs by using the `openedx-filters`_ library and is limited to the following filters per processor:
xAPI Filters
------------
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| Filter | Description |
+=================================================================================================+====================================================================================+
| event_routing_backends.processors.xapi.transformer.xapi_transformer.get_actor | Intercepts and allows to modify the xAPI actor field, this affects all xAPI events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.transformer.xapi_transformer.get_verb | Intercepts and allows to modify the xAPI actor field, this affects all xAPI events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.completion_events.completion_created.get_object | Allows to modify the xAPI object field, this just affects completion events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.enrollment_events.base_enrollment.get_object | Allows to modify the xAPI object field, this just affects enrollment events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.exam_events.base_exam.get_object | Allows to modify the xAPI object field, this just affects exam events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.forum_events.base_forum_thread.get_object | Allows to modify the xAPI object field, this just affects forum events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.grading_events.subsection_graded.get_object | Allows to modify the xAPI object field, this just affects subsection_graded events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.grading_events.course_graded.get_object | Allows to modify the xAPI object field, this just affects course_graded events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.navigation_events.link_clicked.get_object | Allows to modify the xAPI object field, this just affects link_clicked events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.navigation_events.outline_selected.get_object | Allows to modify the xAPI object field, this just affects outline_selected events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.navigation_events.tab_navigation.get_object | Allows to modify the xAPI object field, this just affects tab_navigation events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.problem_interaction_events.base_problems.get_object | Allows to modify the xAPI object field, this just affects problem events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.problem_interaction_events.base_problem_check.get_object | Allows to modify the xAPI object field, this just affects problem_check events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
| event_routing_backends.processors.xapi.video_events.base_video.get_object | Allows to modify the xAPI object field, this affects all video events |
+-------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------+
.. _event-tracking: https://github.com/openedx/event-tracking
.. _NameWhitelist: https://github.com/openedx/event-tracking/blob/master/eventtracking/processors/whitelist.py
Expand All @@ -245,3 +286,6 @@ A sample override for ``xapi`` backend is presented below. Here we are allowing
.. _edx-celeryutils: https://github.com/openedx/edx-celeryutils
.. _commands: https://github.com/openedx/edx-celeryutils/tree/master/celery_utils/management/commands
.. _openedx-filters: https://github.com/openedx/openedx-filters
2 changes: 1 addition & 1 deletion event_routing_backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Various backends for receiving edX LMS events..
"""

__version__ = '7.0.2'
__version__ = '7.1.0'
Empty file.
49 changes: 49 additions & 0 deletions event_routing_backends/processors/openedx_filters/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Decorators that helps to implement the Processor filter functionality.
"""
import functools

from event_routing_backends.processors.openedx_filters.filters import ProcessorBaseFilter


def openedx_filter(filter_type):
"""
This decorator allows to implement the ProcessorBaseFilter on multiple class methods
and intends to modify the returned value from methods like get_actor or get_objects
in cases where the standard output doesn't satisfy the implementation requirements.
Arguments:
filter_type: String that defines the filter_type attribute of ProcessorBaseFilter,
this allows to identify the configuration setting.
Example:
1. Decorate your method:
@openedx_filter(filter_type="this.will.be.the.filter.key")
def get_object(self):
...
2. Set the openedx filter config in your environment variables.
OPEN_EDX_FILTERS_CONFIG = {
"this.will.be.the.filter.key": {
"pipeline": ["path.to.an.external.pipeline.step"],
"fail_silently": False,
}
}
3. More details about filters https://github.com/openedx/openedx-filters/
"""
def wrapper(func):
@functools.wraps(func)
def inner_wrapper(*args, **kwargs):
dynamic_filter = ProcessorBaseFilter.generate_dynamic_filter(filter_type=filter_type)

return dynamic_filter.run_filter(
result=func(*args, **kwargs),
)

return inner_wrapper

return wrapper
10 changes: 10 additions & 0 deletions event_routing_backends/processors/openedx_filters/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Custom processors exceptions thrown by filters.
"""
from openedx_filters.exceptions import OpenEdxFilterException


class InvalidFilterType(OpenEdxFilterException):
"""
Exception that indicates that the attribute `filter_type` has not been set property.
"""
50 changes: 50 additions & 0 deletions event_routing_backends/processors/openedx_filters/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Processors filters, this file aims to contain all the filters that could modify the
standard transformer results by implementing external pipeline steps.
"""
from openedx_filters.tooling import OpenEdxPublicFilter

from event_routing_backends.processors.openedx_filters.exceptions import InvalidFilterType


class ProcessorBaseFilter(OpenEdxPublicFilter):
"""
This is a general filter class that applies the open edx filter in multiple
scenarios, its functionality is limited to one input one output therefore this
only can be applied in method that returns a unique value.
"""

@classmethod
def generate_dynamic_filter(cls, filter_type):
"""This generates a sub class of ProcessorBaseFilter with the filter_type attribute.
Arguments:
filter_type: String the defines the filter key on the OPEN_EDX_FILTERS_CONFIG
section
Returns:
ProcessorBaseFilter sub-class: This new class includes the filter_type attribute.
"""
return type("DynamicFilter", (cls,), {"filter_type": filter_type})

@classmethod
def run_filter(cls, result):
"""
Executes a filter after validating the right class configuration.
Arguments:
result: Result to be modified or extended.
Returns:
result: This value comes from the dictionary returned by run_pipeline and will vary
depends on the implemented pipelines.
Raises:
InvalidFilterType: if the ProcessorBaseFilter is used instead of a dynamic filter.
"""
if not cls.filter_type:
raise InvalidFilterType("Parameter filter_type has not been set.")

data = super().run_pipeline(result=result)

return data.get("result", result)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Test cases for the filters file."""
from django.test import TestCase
from mock import patch
from openedx_filters.tooling import OpenEdxPublicFilter

from event_routing_backends.processors.openedx_filters.exceptions import InvalidFilterType
from event_routing_backends.processors.openedx_filters.filters import ProcessorBaseFilter


class TestProcessorBaseFilter(TestCase):
"""General test cases for the ProcessorBaseFilter class."""

def test_invalid_configuration(self):
"""This test that the exception XApiInvalidFilterType is raised when
the filter_type attribute has not been set.
Expected behavior:
- InvalidFilterType exception is raised
"""
self.assertRaises(InvalidFilterType, ProcessorBaseFilter.run_filter, "dummy_value")

@patch.object(OpenEdxPublicFilter, "run_pipeline")
def test_expected_value(self, run_pipeline_mock):
"""This checks that the method run_filter returns the value generated by
the parent method `run_pipeline`
Expected behavior:
- run_pipeline is called with the right key and value
- run_filter returns the value of the result key
"""
run_pipeline_mock.return_value = {
"result": "expected_value"
}
input_value = "dummy_value"
openedx_filter = ProcessorBaseFilter.generate_dynamic_filter(filter_type="test_filter")

result = openedx_filter.run_filter(result=input_value)

run_pipeline_mock.assert_called_once_with(result=input_value)
self.assertEqual(run_pipeline_mock()["result"], result)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Result, Verb

from event_routing_backends.processors.openedx_filters.decorators import openedx_filter
from event_routing_backends.processors.xapi import constants
from event_routing_backends.processors.xapi.registry import XApiTransformersRegistry
from event_routing_backends.processors.xapi.transformer import XApiTransformer
Expand All @@ -14,13 +15,16 @@ class CompletionCreatedTransformer(XApiTransformer):
Transformers for event generated when an student completion is created or updated.
"""

verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_PROGRESSED,
display=LanguageMap({constants.EN: constants.PROGRESSED}),
)

additional_fields = ('result', )

@openedx_filter(
filter_type="event_routing_backends.processors.xapi.completion_events.completion_created.get_object",
)
def get_object(self):
"""
Get object for xAPI transformed event related to a thread.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Verb

from event_routing_backends.helpers import get_course_from_id
from event_routing_backends.processors.openedx_filters.decorators import openedx_filter
from event_routing_backends.processors.xapi import constants
from event_routing_backends.processors.xapi.registry import XApiTransformersRegistry
from event_routing_backends.processors.xapi.transformer import XApiTransformer
Expand All @@ -25,6 +26,7 @@ def get_context_activities(self):

return None

@openedx_filter(filter_type="event_routing_backends.processors.xapi.enrollment_events.base_enrollment.get_object")
def get_object(self):
"""
Get object for xAPI transformed event.
Expand Down Expand Up @@ -55,7 +57,7 @@ class EnrollmentActivatedTransformer(BaseEnrollmentTransformer):
"""
Transformers for event generated when learner enrolls or gets the enrollment mode changed in a course.
"""
verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_REGISTERED,
display=LanguageMap({constants.EN: constants.REGISTERED}),
)
Expand All @@ -66,7 +68,7 @@ class EnrollmentDeactivatedTransformer(BaseEnrollmentTransformer):
"""
Transformers for event generated when learner un-enrolls from a course.
"""
verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_UNREGISTERED,
display=LanguageMap({constants.EN: constants.UNREGISTERED}),
)
Expand All @@ -77,7 +79,7 @@ class CourseGradePassedFirstTimeTransformer(BaseEnrollmentTransformer):
"""
Transformers for event generated when learner pass course grade first time from a course.
"""
verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_PASSED,
display=LanguageMap({constants.EN: constants.PASSED}),
)
Expand All @@ -88,7 +90,7 @@ class CourseGradeNowPassedTransformer(BaseEnrollmentTransformer):
"""
Transformers for event generated when learner pass course grade first time from a course.
"""
verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_PASSED,
display=LanguageMap({constants.EN: constants.PASSED}),
)
Expand All @@ -99,7 +101,7 @@ class CourseGradeNowFailedTransformer(BaseEnrollmentTransformer):
"""
Transformers for event generated when learner pass course grade first time from a course.
"""
verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_FAILED,
display=LanguageMap({constants.EN: constants.FAILED}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Verb

from event_routing_backends.processors.openedx_filters.decorators import openedx_filter
from event_routing_backends.processors.xapi import constants
from event_routing_backends.processors.xapi.registry import XApiTransformersRegistry
from event_routing_backends.processors.xapi.transformer import XApiTransformer
Expand All @@ -16,6 +17,7 @@ class BaseExamTransformer(XApiTransformer):

exam_type_activity = None

@openedx_filter(filter_type="event_routing_backends.processors.xapi.exam_events.base_exam.get_object")
def get_object(self):
"""
Get object for xAPI transformed event.
Expand Down Expand Up @@ -101,7 +103,7 @@ class InitializedMixin:
Base transformer for initialized exam events
"""

verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_INITIALIZED,
display=LanguageMap({constants.EN: constants.INITIALIZED}),
)
Expand All @@ -112,7 +114,7 @@ class TerminatedMixin:
Base transformer for terminated exam events
"""

verb = Verb(
_verb = Verb(
id=constants.XAPI_VERB_TERMINATED,
display=LanguageMap({constants.EN: constants.TERMINATED}),
)
Expand Down
Loading

0 comments on commit 65ddeae

Please sign in to comment.