From 2dfb6e24fc0d8f7cab5fb412cb446f7461c57ac8 Mon Sep 17 00:00:00 2001 From: Jillian Date: Fri, 21 Jun 2024 10:12:18 +0930 Subject: [PATCH] feat: adds xApiTransforms for completion aggregator events (#205) * chore: adds edx-event-routing-backends requirement @ 9.3.0 * chore: adds 'factory' as a test requirement because ERB tests needs it. * feat: adds xApiTransforms for completion aggregator events * feat: adds the completion_aggregator events to the event tracking whitelist in plugin settings * test: adds transformer and plugin_settings tests * test: make COMPLETION_AGGREGATOR_ASYNC_AGGREGATION consistent between test settings and plugin settings. * chore: bumps version to 4.2.0 * docs: adds note about xAPI to README --------- Co-authored-by: andrey-canon --- .gitignore | 1 + CHANGELOG.rst | 6 ++ README.rst | 2 +- completion_aggregator/__init__.py | 2 +- completion_aggregator/apps.py | 6 +- completion_aggregator/settings/common.py | 21 +++++ completion_aggregator/xapi.py | 71 ++++++++++++++++ requirements/base.in | 1 + requirements/base.txt | 60 +++++++++++-- requirements/ci.txt | 4 +- requirements/dev.txt | 79 +++++++++++++++-- requirements/doc.txt | 84 ++++++++++++++++++- requirements/pip-tools.txt | 6 +- requirements/quality.txt | 76 ++++++++++++++++- requirements/test.in | 1 + requirements/test.txt | 79 +++++++++++++++-- test_output/.gitkeep | 1 + test_settings.py | 8 +- ...ompletion_aggregator.progress.chapter.json | 51 +++++++++++ ...completion_aggregator.progress.course.json | 51 +++++++++++ ...letion_aggregator.progress.sequential.json | 51 +++++++++++ ...mpletion_aggregator.progress.vertical.json | 51 +++++++++++ ...ompletion_aggregator.progress.chapter.json | 30 +++++++ ...completion_aggregator.progress.course.json | 34 ++++++++ ...letion_aggregator.progress.sequential.json | 30 +++++++ ...mpletion_aggregator.progress.vertical.json | 30 +++++++ tests/test_plugin_settings.py | 30 +++++++ tests/test_xapi.py | 75 +++++++++++++++++ 28 files changed, 909 insertions(+), 32 deletions(-) create mode 100644 completion_aggregator/xapi.py create mode 100644 test_output/.gitkeep create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.progress.course.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.progress.chapter.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.progress.course.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.progress.sequential.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.progress.vertical.json create mode 100644 tests/test_xapi.py diff --git a/.gitignore b/.gitignore index b7b99cc3..7b5132ee 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ pip-log.txt .tox coverage.xml htmlcov/ +test_output/*.json # Translations *.mo diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70c28815..c2d8eaef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,12 @@ Change Log Unreleased ~~~~~~~~~~ +[4.2.0] - 2024-06-21 +~~~~~~~~~~~~~~~~~~~~ + +* Transform `openedx.completion_aggregator.progress.*` tracking log events into xAPI using edx-event-routing-backends so + they can be included in Aspects analytics data. + [4.1.0] - 2024-06-18 ~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 0fc4772c..c2662e30 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ For details about how the completion aggregator's REST APIs can be used, please Event tracking -------------- -Like other parts of Open edX, the completion aggregator emits "tracking logs" events whenever completion aggregator records are created or updated by this plugin. These events can be used for analytics, for example to track learner progress in a course. +Like other parts of Open edX, the completion aggregator emits "tracking logs" events whenever completion aggregator records are created or updated by this plugin. These events are transformed into xAPI and routed using `edx-event-routing-backends` so they can be used for analytics, for example to track learner progress in a course. Event tracking is enabled by default for edx-platform, and so event tracking is also enabled by default in the completion aggregator. This can result in a lot of events being generated — for example when a user completes the final block in a course, aggregator completion events will be generated for the containing unit, subsection, section, and course. diff --git a/completion_aggregator/__init__.py b/completion_aggregator/__init__.py index a394c7f7..8778f73b 100644 --- a/completion_aggregator/__init__.py +++ b/completion_aggregator/__init__.py @@ -5,4 +5,4 @@ from __future__ import absolute_import, unicode_literals -__version__ = '4.1.0' +__version__ = '4.2.0' diff --git a/completion_aggregator/apps.py b/completion_aggregator/apps.py index 07595a66..ca58b5a9 100644 --- a/completion_aggregator/apps.py +++ b/completion_aggregator/apps.py @@ -38,6 +38,10 @@ def ready(self): """ Load signal handlers when the app is ready. """ + # pylint: disable=import-outside-toplevel from . import signals signals.register() - from .tasks import aggregation_tasks, handler_tasks # pylint: disable=unused-import + + # pylint: disable=unused-import + from . import xapi + from .tasks import aggregation_tasks, handler_tasks diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index a4b8f0b3..c60cd4db 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -4,6 +4,8 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from event_routing_backends.utils.settings import event_tracking_backends_config + def plugin_settings(settings): """ @@ -42,3 +44,22 @@ def plugin_settings(settings): # 1. All courses should be reaggregated for the changes to take effect. # 2. It's not possible to revert this change by reaggregation without manually removing existing Aggregators. settings.COMPLETION_AGGREGATOR_AGGREGATE_UNRELEASED_BLOCKS = False + + # Whitelist the aggregator events for use with event routing backends xAPI backend. + # If these settings don't already exist, then ERB hasn't been loaded yet, so we need to set them to empty lists. + # But once ERB does load it will append its events to our list, preserving what we added here. + if not hasattr(settings, 'EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS'): + settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS = [] + if not hasattr(settings, 'EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS'): + settings.EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS = [] + enabled_aggregator_events = [ + f'openedx.completion_aggregator.progress.{block_type}' + + for block_type in settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES + ] + settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS += enabled_aggregator_events + settings.EVENT_TRACKING_BACKENDS.update(event_tracking_backends_config( + settings, + settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS, + settings.EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS, + )) diff --git a/completion_aggregator/xapi.py b/completion_aggregator/xapi.py new file mode 100644 index 00000000..99992514 --- /dev/null +++ b/completion_aggregator/xapi.py @@ -0,0 +1,71 @@ +""" +Transformers for completion aggregation. +""" + +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 +from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Result, Verb + + +class BaseProgressTransformer(XApiTransformer): + """ + Base transformer for completion aggregator progress events. + """ + + _verb = Verb( + id=constants.XAPI_VERB_PROGRESSED, + display=LanguageMap({constants.EN: constants.PROGRESSED}), + ) + object_type = None + additional_fields = ('result', ) + + @openedx_filter( + filter_type="completion_aggregator.xapi.progress.get_object", + ) + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + if not self.object_type: + raise NotImplementedError() # pragma: no cover + + return Activity( + id=self.get_object_iri("xblock", self.get_data("data.block_id")), + definition=ActivityDefinition( + type=self.object_type, + ), + ) + + def get_result(self) -> Result: + """ + Get result for xAPI transformed event. + """ + progress = self.get_data("data.percent") or 0 + return Result( + completion=progress == 1.0, + extensions=Extensions({ + constants.XAPI_ACTIVITY_PROGRESS: (progress * 100), + }), + ) + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.chapter") +@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.sequential") +@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.vertical") +class ModuleProgressTransformer(BaseProgressTransformer): + """ + Transformer for event generated when a user makes progress in a section, subsection or unit. + """ + + object_type = constants.XAPI_ACTIVITY_MODULE + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.course") +class CourseProgressTransformer(BaseProgressTransformer): + """ + Transformer for event generated when a user makes progress in a course. + """ + + object_type = constants.XAPI_ACTIVITY_COURSE diff --git a/requirements/base.in b/requirements/base.in index 8355867e..20f5a3fc 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -12,3 +12,4 @@ edx-toggles event-tracking # Allows the aggregator to emit tracking events six XBlock[django] +edx-event-routing-backends # Provides xAPI transforms for aggregator events diff --git a/requirements/base.txt b/requirements/base.txt index 2744713d..a77c7ca6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,10 +6,16 @@ # amqp==5.2.0 # via kombu +aniso8601==9.0.1 + # via tincan +apache-libcloud==3.8.0 + # via edx-event-routing-backends appdirs==1.4.4 # via fs asgiref==3.7.2 # via django +async-timeout==4.0.3 + # via redis attrs==23.2.0 # via openedx-events backports-zoneinfo[tzdata]==0.2.1 @@ -54,13 +60,18 @@ click-repl==0.3.0 code-annotations==1.6.0 # via edx-toggles cryptography==42.0.5 - # via pyjwt + # via + # django-fernet-fields-v2 + # pyjwt django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # django-config-models # django-crum + # django-fernet-fields-v2 # django-model-utils + # django-redis # django-waffle # djangorestframework # drf-jwt @@ -68,20 +79,28 @@ django==3.2.24 # edx-completion # edx-django-utils # edx-drf-extensions + # edx-event-routing-backends # edx-toggles # event-tracking # jsonfield # openedx-django-pyfs # openedx-events + # openedx-filters +django-config-models==2.7.0 + # via edx-event-routing-backends django-crum==0.7.9 # via # edx-django-utils # edx-toggles +django-fernet-fields-v2==0.9 + # via edx-event-routing-backends django-model-utils==4.4.0 # via # -r requirements/base.in # edx-celeryutils # edx-completion +django-redis==5.4.0 + # via edx-event-routing-backends django-waffle==4.1.0 # via # edx-django-utils @@ -90,23 +109,31 @@ django-waffle==4.1.0 djangorestframework==3.14.0 # via # -r requirements/base.in + # django-config-models # drf-jwt # edx-completion # edx-drf-extensions drf-jwt==1.19.2 # via edx-drf-extensions edx-celeryutils==1.2.5 - # via -r requirements/base.in + # via + # -r requirements/base.in + # edx-event-routing-backends edx-completion==4.6.0 # via -r requirements/base.in edx-django-utils==5.10.1 # via + # django-config-models # edx-drf-extensions # edx-toggles # event-tracking # openedx-events edx-drf-extensions==10.2.0 # via edx-completion +edx-event-routing-backends==9.3.0 + # via + # -c requirements/constraints.txt + # -r requirements/base.in edx-opaque-keys[django]==2.5.1 # via # -r requirements/base.in @@ -117,13 +144,17 @@ edx-toggles==5.1.1 # via # -r requirements/base.in # edx-completion + # edx-event-routing-backends # event-tracking -event-tracking==2.3.0 +event-tracking==2.4.0 # via # -r requirements/base.in - # edx-completion + # edx-completion + # edx-event-routing-backends fastavro==1.9.4 # via openedx-events +fasteners==0.19 + # via edx-event-routing-backends fs==2.4.16 # via # fs-s3fs @@ -133,6 +164,8 @@ fs-s3fs==1.1.1 # via openedx-django-pyfs idna==3.6 # via requests +isodate==0.6.1 + # via edx-event-routing-backends jinja2==3.1.3 # via code-annotations jmespath==1.0.1 @@ -140,7 +173,9 @@ jmespath==1.0.1 # boto3 # botocore jsonfield==3.1.0 - # via edx-celeryutils + # via + # edx-celeryutils + # edx-event-routing-backends kombu==5.3.5 # via celery lazy==1.6 @@ -160,6 +195,8 @@ openedx-django-pyfs==3.5.0 # via xblock openedx-events==9.5.2 # via event-tracking +openedx-filters==1.8.1 + # via edx-event-routing-backends pbr==6.0.0 # via stevedore prompt-toolkit==3.0.43 @@ -182,6 +219,7 @@ python-dateutil==2.8.2 # via # botocore # celery + # edx-event-routing-backends # xblock python-slugify==8.0.4 # via code-annotations @@ -190,14 +228,21 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via # code-annotations # xblock +redis==5.0.5 + # via django-redis requests==2.31.0 - # via edx-drf-extensions + # via + # apache-libcloud + # edx-drf-extensions + # edx-event-routing-backends s3transfer==0.10.0 # via boto3 semantic-version==2.10.0 @@ -210,6 +255,7 @@ six==1.16.0 # event-tracking # fs # fs-s3fs + # isodate # python-dateutil sqlparse==0.4.4 # via django @@ -220,6 +266,8 @@ stevedore==5.2.0 # edx-opaque-keys text-unidecode==1.3 # via python-slugify +tincan==1.0.0 + # via edx-event-routing-backends typing-extensions==4.10.0 # via # asgiref diff --git a/requirements/ci.txt b/requirements/ci.txt index 62990009..9bf089d7 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -13,9 +13,7 @@ filelock==3.13.1 packaging==23.2 # via tox platformdirs==4.2.0 - # via - # tox - # virtualenv + # via virtualenv pluggy==0.13.1 # via # -c requirements/constraints.txt diff --git a/requirements/dev.txt b/requirements/dev.txt index b398bce8..a2429c01 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,6 +8,14 @@ amqp==5.2.0 # via # -r requirements/quality.txt # kombu +aniso8601==9.0.1 + # via + # -r requirements/quality.txt + # tincan +apache-libcloud==3.8.0 + # via + # -r requirements/quality.txt + # edx-event-routing-backends appdirs==1.4.4 # via # -r requirements/quality.txt @@ -111,6 +119,7 @@ coverage[toml]==7.4.3 cryptography==42.0.5 # via # -r requirements/quality.txt + # django-fernet-fields-v2 # jwcrypto # pyjwt ddt==1.7.1 @@ -127,9 +136,12 @@ django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/quality.txt + # django-config-models # django-crum + # django-fernet-fields-v2 # django-model-utils # django-oauth-toolkit + # django-redis # django-waffle # djangorestframework # drf-jwt @@ -137,17 +149,27 @@ django==3.2.24 # edx-completion # edx-django-utils # edx-drf-extensions + # edx-event-routing-backends # edx-i18n-tools # edx-toggles # event-tracking # jsonfield # openedx-django-pyfs # openedx-events + # openedx-filters +django-config-models==2.7.0 + # via + # -r requirements/quality.txt + # edx-event-routing-backends django-crum==0.7.9 # via # -r requirements/quality.txt # edx-django-utils # edx-toggles +django-fernet-fields-v2==0.9 + # via + # -r requirements/quality.txt + # edx-event-routing-backends django-model-utils==4.4.0 # via # -r requirements/quality.txt @@ -155,6 +177,10 @@ django-model-utils==4.4.0 # edx-completion django-oauth-toolkit==2.3.0 # via -r requirements/quality.txt +django-redis==5.4.0 + # via + # -r requirements/quality.txt + # edx-event-routing-backends django-waffle==4.1.0 # via # -r requirements/quality.txt @@ -164,6 +190,7 @@ django-waffle==4.1.0 djangorestframework==3.14.0 # via # -r requirements/quality.txt + # django-config-models # drf-jwt # edx-completion # edx-drf-extensions @@ -172,12 +199,15 @@ drf-jwt==1.19.2 # -r requirements/quality.txt # edx-drf-extensions edx-celeryutils==1.2.5 - # via -r requirements/quality.txt + # via + # -r requirements/quality.txt + # edx-event-routing-backends edx-completion==4.6.0 # via -r requirements/quality.txt edx-django-utils==5.10.1 # via # -r requirements/quality.txt + # django-config-models # edx-drf-extensions # edx-toggles # event-tracking @@ -186,6 +216,10 @@ edx-drf-extensions==10.2.0 # via # -r requirements/quality.txt # edx-completion +edx-event-routing-backends==9.3.0 + # via + # -c requirements/constraints.txt + # -r requirements/quality.txt edx-i18n-tools==1.3.0 # via -r requirements/quality.txt edx-lint==5.3.6 @@ -200,15 +234,27 @@ edx-toggles==5.1.1 # via # -r requirements/quality.txt # edx-completion + # edx-event-routing-backends # event-tracking -event-tracking==2.3.0 +event-tracking==2.4.0 # via # -r requirements/quality.txt # edx-completion + # edx-event-routing-backends +factory-boy==3.3.0 + # via -r requirements/quality.txt +faker==25.8.0 + # via + # -r requirements/quality.txt + # factory-boy fastavro==1.9.4 # via # -r requirements/quality.txt # openedx-events +fasteners==0.19 + # via + # -r requirements/quality.txt + # edx-event-routing-backends filelock==3.13.1 # via # -r requirements/ci.txt @@ -230,10 +276,15 @@ idna==3.6 # via # -r requirements/quality.txt # requests -importlib-metadata==7.0.1 +importlib-metadata==6.11.0 # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/pip-tools.txt # build +isodate==0.6.1 + # via + # -r requirements/quality.txt + # edx-event-routing-backends isort==5.13.2 # via # -r requirements/quality.txt @@ -251,6 +302,7 @@ jsonfield==3.1.0 # via # -r requirements/quality.txt # edx-celeryutils + # edx-event-routing-backends jwcrypto==1.5.4 # via # -r requirements/quality.txt @@ -306,6 +358,10 @@ openedx-events==9.5.2 # via # -r requirements/quality.txt # event-tracking +openedx-filters==1.8.1 + # via + # -r requirements/quality.txt + # edx-event-routing-backends packaging==23.2 # via # -r requirements/ci.txt @@ -417,6 +473,8 @@ python-dateutil==2.8.2 # -r requirements/quality.txt # botocore # celery + # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 @@ -429,7 +487,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -437,13 +497,17 @@ pyyaml==6.0.1 # code-annotations # edx-i18n-tools # xblock -redis==5.0.1 - # via -r requirements/quality.txt +redis==5.0.5 + # via + # -r requirements/quality.txt + # django-redis requests==2.31.0 # via # -r requirements/quality.txt + # apache-libcloud # django-oauth-toolkit # edx-drf-extensions + # edx-event-routing-backends s3transfer==0.10.0 # via # -r requirements/quality.txt @@ -465,6 +529,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -487,6 +552,10 @@ text-unidecode==1.3 # via # -r requirements/quality.txt # python-slugify +tincan==1.0.0 + # via + # -r requirements/quality.txt + # edx-event-routing-backends tomli==2.0.1 # via # -r requirements/ci.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 17b75bf1..ffa1e216 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -11,6 +11,16 @@ amqp==5.2.0 # -r requirements/base.txt # -r requirements/test.txt # kombu +aniso8601==9.0.1 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # tincan +apache-libcloud==3.8.0 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends appdirs==1.4.4 # via # -r requirements/base.txt @@ -23,6 +33,7 @@ asgiref==3.7.2 # django async-timeout==4.0.3 # via + # -r requirements/base.txt # -r requirements/test.txt # redis attrs==23.2.0 @@ -115,6 +126,7 @@ cryptography==42.0.5 # via # -r requirements/base.txt # -r requirements/test.txt + # django-fernet-fields-v2 # jwcrypto # pyjwt ddt==1.7.1 @@ -123,9 +135,12 @@ django==3.2.24 # via # -r requirements/base.txt # -r requirements/test.txt + # django-config-models # django-crum + # django-fernet-fields-v2 # django-model-utils # django-oauth-toolkit + # django-redis # django-waffle # djangorestframework # drf-jwt @@ -133,18 +148,30 @@ django==3.2.24 # edx-completion # edx-django-utils # edx-drf-extensions + # edx-event-routing-backends # edx-i18n-tools # edx-toggles # event-tracking # jsonfield # openedx-django-pyfs # openedx-events + # openedx-filters +django-config-models==2.7.0 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends django-crum==0.7.9 # via # -r requirements/base.txt # -r requirements/test.txt # edx-django-utils # edx-toggles +django-fernet-fields-v2==0.9 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends django-model-utils==4.4.0 # via # -r requirements/base.txt @@ -153,6 +180,11 @@ django-model-utils==4.4.0 # edx-completion django-oauth-toolkit==2.3.0 # via -r requirements/test.txt +django-redis==5.4.0 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends django-waffle==4.1.0 # via # -r requirements/base.txt @@ -164,6 +196,7 @@ djangorestframework==3.14.0 # via # -r requirements/base.txt # -r requirements/test.txt + # django-config-models # drf-jwt # edx-completion # edx-drf-extensions @@ -184,6 +217,7 @@ edx-celeryutils==1.2.5 # via # -r requirements/base.txt # -r requirements/test.txt + # edx-event-routing-backends edx-completion==4.6.0 # via # -r requirements/base.txt @@ -192,6 +226,7 @@ edx-django-utils==5.10.1 # via # -r requirements/base.txt # -r requirements/test.txt + # django-config-models # edx-drf-extensions # edx-toggles # event-tracking @@ -201,6 +236,10 @@ edx-drf-extensions==10.2.0 # -r requirements/base.txt # -r requirements/test.txt # edx-completion +edx-event-routing-backends==9.3.0 + # via + # -r requirements/base.txt + # -r requirements/test.txt edx-i18n-tools==1.3.0 # via -r requirements/test.txt edx-opaque-keys[django]==2.5.1 @@ -217,17 +256,30 @@ edx-toggles==5.1.1 # -r requirements/base.txt # -r requirements/test.txt # edx-completion + # edx-event-routing-backends # event-tracking -event-tracking==2.3.0 +event-tracking==2.4.0 # via # -r requirements/base.txt # -r requirements/test.txt # edx-completion + # edx-event-routing-backends +factory-boy==3.3.0 + # via -r requirements/test.txt +faker==25.8.0 + # via + # -r requirements/test.txt + # factory-boy fastavro==1.9.4 # via # -r requirements/base.txt # -r requirements/test.txt # openedx-events +fasteners==0.19 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends freezegun==0.3.15 # via -r requirements/test.txt fs==2.4.16 @@ -251,6 +303,11 @@ imagesize==1.4.1 # via sphinx importlib-metadata==7.0.1 # via sphinx +isodate==0.6.1 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends jinja2==3.1.3 # via # -r requirements/base.txt @@ -268,6 +325,7 @@ jsonfield==3.1.0 # -r requirements/base.txt # -r requirements/test.txt # edx-celeryutils + # edx-event-routing-backends jwcrypto==1.5.4 # via # -r requirements/test.txt @@ -329,6 +387,11 @@ openedx-events==9.5.2 # -r requirements/base.txt # -r requirements/test.txt # event-tracking +openedx-filters==1.8.1 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends packaging==23.2 # via # -r requirements/test.txt @@ -407,6 +470,8 @@ python-dateutil==2.8.2 # -r requirements/test.txt # botocore # celery + # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 @@ -422,7 +487,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -433,14 +500,19 @@ pyyaml==6.0.1 # xblock readme-renderer==42.0 # via -r requirements/doc.in -redis==5.0.1 - # via -r requirements/test.txt +redis==5.0.5 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # django-redis requests==2.31.0 # via # -r requirements/base.txt # -r requirements/test.txt + # apache-libcloud # django-oauth-toolkit # edx-drf-extensions + # edx-event-routing-backends # sphinx restructuredtext-lint==1.4.0 # via doc8 @@ -468,6 +540,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -507,6 +580,11 @@ text-unidecode==1.3 # -r requirements/base.txt # -r requirements/test.txt # python-slugify +tincan==1.0.0 + # via + # -r requirements/base.txt + # -r requirements/test.txt + # edx-event-routing-backends tomli==2.0.1 # via # -r requirements/test.txt diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 44c48d99..294295fd 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -8,8 +8,10 @@ build==1.0.3 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==7.0.1 - # via build +importlib-metadata==6.11.0 + # via + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt + # build packaging==23.2 # via build pip-tools==7.4.0 diff --git a/requirements/quality.txt b/requirements/quality.txt index 4ef12726..69f2f828 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -8,6 +8,14 @@ amqp==5.2.0 # via # -r requirements/test.txt # kombu +aniso8601==9.0.1 + # via + # -r requirements/test.txt + # tincan +apache-libcloud==3.8.0 + # via + # -r requirements/test.txt + # edx-event-routing-backends appdirs==1.4.4 # via # -r requirements/test.txt @@ -102,6 +110,7 @@ coverage[toml]==7.4.3 cryptography==42.0.5 # via # -r requirements/test.txt + # django-fernet-fields-v2 # jwcrypto # pyjwt ddt==1.7.1 @@ -112,9 +121,12 @@ django==3.2.24 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/test.txt + # django-config-models # django-crum + # django-fernet-fields-v2 # django-model-utils # django-oauth-toolkit + # django-redis # django-waffle # djangorestframework # drf-jwt @@ -122,17 +134,27 @@ django==3.2.24 # edx-completion # edx-django-utils # edx-drf-extensions + # edx-event-routing-backends # edx-i18n-tools # edx-toggles # event-tracking # jsonfield # openedx-django-pyfs # openedx-events + # openedx-filters +django-config-models==2.7.0 + # via + # -r requirements/test.txt + # edx-event-routing-backends django-crum==0.7.9 # via # -r requirements/test.txt # edx-django-utils # edx-toggles +django-fernet-fields-v2==0.9 + # via + # -r requirements/test.txt + # edx-event-routing-backends django-model-utils==4.4.0 # via # -r requirements/test.txt @@ -140,6 +162,10 @@ django-model-utils==4.4.0 # edx-completion django-oauth-toolkit==2.3.0 # via -r requirements/test.txt +django-redis==5.4.0 + # via + # -r requirements/test.txt + # edx-event-routing-backends django-waffle==4.1.0 # via # -r requirements/test.txt @@ -149,6 +175,7 @@ django-waffle==4.1.0 djangorestframework==3.14.0 # via # -r requirements/test.txt + # django-config-models # drf-jwt # edx-completion # edx-drf-extensions @@ -157,12 +184,15 @@ drf-jwt==1.19.2 # -r requirements/test.txt # edx-drf-extensions edx-celeryutils==1.2.5 - # via -r requirements/test.txt + # via + # -r requirements/test.txt + # edx-event-routing-backends edx-completion==4.6.0 # via -r requirements/test.txt edx-django-utils==5.10.1 # via # -r requirements/test.txt + # django-config-models # edx-drf-extensions # edx-toggles # event-tracking @@ -171,6 +201,10 @@ edx-drf-extensions==10.2.0 # via # -r requirements/test.txt # edx-completion +edx-event-routing-backends==9.3.0 + # via + # -c requirements/constraints.txt + # -r requirements/test.txt edx-i18n-tools==1.3.0 # via -r requirements/test.txt edx-lint==5.3.6 @@ -185,15 +219,27 @@ edx-toggles==5.1.1 # via # -r requirements/test.txt # edx-completion + # edx-event-routing-backends # event-tracking -event-tracking==2.3.0 +event-tracking==2.4.0 # via # -r requirements/test.txt # edx-completion + # edx-event-routing-backends +factory-boy==3.3.0 + # via -r requirements/test.txt +faker==25.8.0 + # via + # -r requirements/test.txt + # factory-boy fastavro==1.9.4 # via # -r requirements/test.txt # openedx-events +fasteners==0.19 + # via + # -r requirements/test.txt + # edx-event-routing-backends freezegun==0.3.15 # via -r requirements/test.txt fs==2.4.16 @@ -210,6 +256,10 @@ idna==3.6 # via # -r requirements/test.txt # requests +isodate==0.6.1 + # via + # -r requirements/test.txt + # edx-event-routing-backends isort==5.13.2 # via # -r requirements/quality.in @@ -227,6 +277,7 @@ jsonfield==3.1.0 # via # -r requirements/test.txt # edx-celeryutils + # edx-event-routing-backends jwcrypto==1.5.4 # via # -r requirements/test.txt @@ -280,6 +331,10 @@ openedx-events==9.5.2 # via # -r requirements/test.txt # event-tracking +openedx-filters==1.8.1 + # via + # -r requirements/test.txt + # edx-event-routing-backends packaging==23.2 # via # -r requirements/test.txt @@ -366,6 +421,8 @@ python-dateutil==2.8.2 # -r requirements/test.txt # botocore # celery + # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 @@ -378,7 +435,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -386,13 +445,17 @@ pyyaml==6.0.1 # code-annotations # edx-i18n-tools # xblock -redis==5.0.1 - # via -r requirements/test.txt +redis==5.0.5 + # via + # -r requirements/test.txt + # django-redis requests==2.31.0 # via # -r requirements/test.txt + # apache-libcloud # django-oauth-toolkit # edx-drf-extensions + # edx-event-routing-backends s3transfer==0.10.0 # via # -r requirements/test.txt @@ -413,6 +476,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -432,6 +496,10 @@ text-unidecode==1.3 # via # -r requirements/test.txt # python-slugify +tincan==1.0.0 + # via + # -r requirements/test.txt + # edx-event-routing-backends tomli==2.0.1 # via # -r requirements/test.txt diff --git a/requirements/test.in b/requirements/test.in index 6371b860..2bc7a2d2 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -16,3 +16,4 @@ django-oauth-toolkit mysqlclient # For connecting to MySQL edx-i18n-tools # For i18n_tool dummy +factory-boy # For event transformers diff --git a/requirements/test.txt b/requirements/test.txt index 041bb5ae..2dd35c8f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -8,6 +8,14 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +aniso8601==9.0.1 + # via + # -r requirements/base.txt + # tincan +apache-libcloud==3.8.0 + # via + # -r requirements/base.txt + # edx-event-routing-backends appdirs==1.4.4 # via # -r requirements/base.txt @@ -17,7 +25,9 @@ asgiref==3.7.2 # -r requirements/base.txt # django async-timeout==4.0.3 - # via redis + # via + # -r requirements/base.txt + # redis attrs==23.2.0 # via # -r requirements/base.txt @@ -89,6 +99,7 @@ coverage[toml]==7.4.3 cryptography==42.0.5 # via # -r requirements/base.txt + # django-fernet-fields-v2 # jwcrypto # pyjwt ddt==1.7.1 @@ -96,9 +107,12 @@ ddt==1.7.1 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.txt + # django-config-models # django-crum + # django-fernet-fields-v2 # django-model-utils # django-oauth-toolkit + # django-redis # django-waffle # djangorestframework # drf-jwt @@ -106,17 +120,27 @@ ddt==1.7.1 # edx-completion # edx-django-utils # edx-drf-extensions + # edx-event-routing-backends # edx-i18n-tools # edx-toggles # event-tracking # jsonfield # openedx-django-pyfs # openedx-events + # openedx-filters +django-config-models==2.7.0 + # via + # -r requirements/base.txt + # edx-event-routing-backends django-crum==0.7.9 # via # -r requirements/base.txt # edx-django-utils # edx-toggles +django-fernet-fields-v2==0.9 + # via + # -r requirements/base.txt + # edx-event-routing-backends django-model-utils==4.4.0 # via # -r requirements/base.txt @@ -125,6 +149,10 @@ django-model-utils==4.4.0 # edx-completion django-oauth-toolkit==2.3.0 # via -r requirements/test.in +django-redis==5.4.0 + # via + # -r requirements/base.txt + # edx-event-routing-backends django-waffle==4.1.0 # via # -r requirements/base.txt @@ -134,6 +162,7 @@ django-waffle==4.1.0 djangorestframework==3.14.0 # via # -r requirements/base.txt + # django-config-models # drf-jwt # edx-completion # edx-drf-extensions @@ -142,12 +171,15 @@ drf-jwt==1.19.2 # -r requirements/base.txt # edx-drf-extensions edx-celeryutils==1.2.5 - # via -r requirements/base.txt + # via + # -r requirements/base.txt + # edx-event-routing-backends edx-completion==4.6.0 # via -r requirements/base.txt edx-django-utils==5.10.1 # via # -r requirements/base.txt + # django-config-models # edx-drf-extensions # edx-toggles # event-tracking @@ -156,6 +188,10 @@ edx-drf-extensions==10.2.0 # via # -r requirements/base.txt # edx-completion +edx-event-routing-backends==9.3.0 + # via + # -c requirements/constraints.txt + # -r requirements/base.txt edx-i18n-tools==1.3.0 # via -r requirements/test.in edx-opaque-keys[django]==2.5.1 @@ -168,15 +204,25 @@ edx-toggles==5.1.1 # via # -r requirements/base.txt # edx-completion + # edx-event-routing-backends # event-tracking -event-tracking==2.3.0 +event-tracking==2.4.0 # via # -r requirements/base.txt # edx-completion + # edx-event-routing-backends +factory-boy==3.3.0 + # via -r requirements/test.in +faker==25.8.0 + # via factory-boy fastavro==1.9.4 # via # -r requirements/base.txt # openedx-events +fasteners==0.19 + # via + # -r requirements/base.txt + # edx-event-routing-backends freezegun==0.3.15 # via -r requirements/test.in fs==2.4.16 @@ -193,6 +239,10 @@ idna==3.6 # via # -r requirements/base.txt # requests +isodate==0.6.1 + # via + # -r requirements/base.txt + # edx-event-routing-backends jinja2==3.1.3 # via # -r requirements/base.txt @@ -206,6 +256,7 @@ jsonfield==3.1.0 # via # -r requirements/base.txt # edx-celeryutils + # edx-event-routing-backends jwcrypto==1.5.4 # via django-oauth-toolkit kombu==5.3.5 @@ -253,6 +304,10 @@ openedx-events==9.5.2 # via # -r requirements/base.txt # event-tracking +openedx-filters==1.8.1 + # via + # -r requirements/base.txt + # edx-event-routing-backends packaging==23.2 # via pytest path==16.10.0 @@ -310,6 +365,8 @@ python-dateutil==2.8.2 # -r requirements/base.txt # botocore # celery + # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 @@ -322,7 +379,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -330,13 +389,18 @@ pyyaml==6.0.1 # code-annotations # edx-i18n-tools # xblock -redis==5.0.1 - # via -r requirements/test.in +redis==5.0.5 + # via + # -r requirements/base.txt + # -r requirements/test.in + # django-redis requests==2.31.0 # via # -r requirements/base.txt + # apache-libcloud # django-oauth-toolkit # edx-drf-extensions + # edx-event-routing-backends s3transfer==0.10.0 # via # -r requirements/base.txt @@ -356,6 +420,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -373,6 +438,10 @@ text-unidecode==1.3 # via # -r requirements/base.txt # python-slugify +tincan==1.0.0 + # via + # -r requirements/base.txt + # edx-event-routing-backends tomli==2.0.1 # via coverage typing-extensions==4.10.0 diff --git a/test_output/.gitkeep b/test_output/.gitkeep new file mode 100644 index 00000000..91cf7fb0 --- /dev/null +++ b/test_output/.gitkeep @@ -0,0 +1 @@ +# Output dir for failed transformer tests diff --git a/test_settings.py b/test_settings.py index 230bdad2..f634d756 100644 --- a/test_settings.py +++ b/test_settings.py @@ -49,12 +49,13 @@ def root(*args): 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.admin', - 'completion_aggregator', 'completion', 'oauth2_provider', 'waffle', 'test_utils.test_app', 'eventtracking.django.apps.EventTrackingConfig', + 'event_routing_backends', + 'completion_aggregator', ) LOCALE_PATHS = [root('completion_aggregator', 'conf', 'locale')] @@ -85,6 +86,11 @@ def root(*args): # Enables event tracking in the tests, see https://github.com/openedx/event-tracking EVENT_TRACKING_ENABLED = True +EVENT_TRACKING_BACKENDS = {} +# Provided so the generated xAPI events use a known LMS URL when testing. +LMS_ROOT_URL = "http://localhost:18000" +# Provided so the generated xAPI events use a known "event routing backends package string" when testing. +RUNNING_WITH_TEST_SETTINGS = True # pylint: disable=unused-import,wrong-import-position from test_utils.test_app import celery # isort:skip diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json new file mode 100644 index 00000000..f8a8cb8f --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json @@ -0,0 +1,51 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"146d5372-1d64-54b1-8c60-b4acaad3c976", + "object":{ + "id":"http://localhost:18000/xblock/block-v1:edX+DemoX+Demo_Course+type@chapter+block@b443e0d6bc4d43c1bed991dbd8a10d42", + "definition":{ + "type":"http://adlnet.gov/expapi/activities/module" + }, + "objectType":"Activity" + }, + "verb":{ + "id":"http://adlnet.gov/expapi/verbs/progressed", + "display":{ + "en":"progressed" + } + }, + "version":"1.0.3", + "context":{ + "contextActivities":{ + "parent":[ + { + "id":"http://localhost:18000/course/course-v1:edX+DemoX+Demo_Course", + "objectType":"Activity", + "definition":{ + "name":{ + "en-US":"Demonstration Course" + }, + "type":"http://adlnet.gov/expapi/activities/course" + } + } + ] + }, + "extensions":{ + "https://w3id.org/xapi/openedx/extension/transformer-version":"event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id":"056aca2a1c6b76742b283e73d3424453" + } + }, + "result":{ + "completion":false, + "extensions": { + "https://w3id.org/xapi/cmi5/result/extensions/progress":50 + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json new file mode 100644 index 00000000..2b089dc5 --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json @@ -0,0 +1,51 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"146d5372-1d64-54b1-8c60-b4acaad3c976", + "object":{ + "id":"http://localhost:18000/xblock/block-v1:edX+DemoX+Demo_Course+type@course+block@course", + "definition":{ + "type":"http://adlnet.gov/expapi/activities/course" + }, + "objectType":"Activity" + }, + "verb":{ + "id":"http://adlnet.gov/expapi/verbs/progressed", + "display":{ + "en":"progressed" + } + }, + "version":"1.0.3", + "context":{ + "contextActivities":{ + "parent":[ + { + "id":"http://localhost:18000/course/course-v1:edX+DemoX+Demo_Course", + "objectType":"Activity", + "definition":{ + "name":{ + "en-US":"Demonstration Course" + }, + "type":"http://adlnet.gov/expapi/activities/course" + } + } + ] + }, + "extensions":{ + "https://w3id.org/xapi/openedx/extension/transformer-version":"event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id":"056aca2a1c6b76742b283e73d3424453" + } + }, + "result":{ + "completion":false, + "extensions": { + "https://w3id.org/xapi/cmi5/result/extensions/progress":80 + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json new file mode 100644 index 00000000..6a6aa381 --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json @@ -0,0 +1,51 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"146d5372-1d64-54b1-8c60-b4acaad3c976", + "object":{ + "id":"http://localhost:18000/xblock/block-v1:edX+DemoX+Demo_Course+type@sequential+block@bf1eac10ebb649e3aaf9cc07325f8e04", + "definition":{ + "type":"http://adlnet.gov/expapi/activities/module" + }, + "objectType":"Activity" + }, + "verb":{ + "id":"http://adlnet.gov/expapi/verbs/progressed", + "display":{ + "en":"progressed" + } + }, + "version":"1.0.3", + "context":{ + "contextActivities":{ + "parent":[ + { + "id":"http://localhost:18000/course/course-v1:edX+DemoX+Demo_Course", + "objectType":"Activity", + "definition":{ + "name":{ + "en-US":"Demonstration Course" + }, + "type":"http://adlnet.gov/expapi/activities/course" + } + } + ] + }, + "extensions":{ + "https://w3id.org/xapi/openedx/extension/transformer-version":"event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id":"056aca2a1c6b76742b283e73d3424453" + } + }, + "result":{ + "completion":false, + "extensions": { + "https://w3id.org/xapi/cmi5/result/extensions/progress":60 + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json new file mode 100644 index 00000000..b2692614 --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json @@ -0,0 +1,51 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"146d5372-1d64-54b1-8c60-b4acaad3c976", + "object":{ + "id":"http://localhost:18000/xblock/block-v1:edX+DemoX+Demo_Course+type@vertical+block@e1fabd9fa55f441caa75580f258ffbc3", + "definition":{ + "type":"http://adlnet.gov/expapi/activities/module" + }, + "objectType":"Activity" + }, + "verb":{ + "id":"http://adlnet.gov/expapi/verbs/progressed", + "display":{ + "en":"progressed" + } + }, + "version":"1.0.3", + "context":{ + "contextActivities":{ + "parent":[ + { + "id":"http://localhost:18000/course/course-v1:edX+DemoX+Demo_Course", + "objectType":"Activity", + "definition":{ + "name":{ + "en-US":"Demonstration Course" + }, + "type":"http://adlnet.gov/expapi/activities/course" + } + } + ] + }, + "extensions":{ + "https://w3id.org/xapi/openedx/extension/transformer-version":"event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id":"056aca2a1c6b76742b283e73d3424453" + } + }, + "result":{ + "completion":true, + "extensions": { + "https://w3id.org/xapi/cmi5/result/extensions/progress":100 + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/raw/openedx.completion_aggregator.progress.chapter.json b/tests/fixtures/raw/openedx.completion_aggregator.progress.chapter.json new file mode 100644 index 00000000..007e09a7 --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.progress.chapter.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.progress.chapter", + "timestamp": "2023-12-05T21:34:52.909063+00:00", + "data": { + "user_id": 4, + "course_id": "course-v1:edX+DemoX+Demo_Course", + "context_key": "course-v1:edX+DemoX+Demo_Course", + "block_id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@b443e0d6bc4d43c1bed991dbd8a10d42", + "block_type": "course", + "percent": 0.5, + "earned": 5, + "possible": 10 + }, + "context": { + "course_id": "course-v1:edX+DemoX+Demo_Course", + "course_user_tags": {}, + "session": "056aca2a1c6b76742b283e73d3424453", + "user_id": 3, + "username": "openedx", + "ip": "172.18.0.1", + "host": "localhost:18000", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", + "path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@problem+block@7c54b16c8ed34f9f8772015178c7a175/handler/xmodule_handler/problem_check", + "referer": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course+type@vertical+block@dd8110c941b94d929b56841195213797?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view", + "accept_language": "en-US,en;q=0.9,es;q=0.8", + "client_id": null, + "org_id": "edX", + "enterprise_uuid": "" + } +} diff --git a/tests/fixtures/raw/openedx.completion_aggregator.progress.course.json b/tests/fixtures/raw/openedx.completion_aggregator.progress.course.json new file mode 100644 index 00000000..dbd5ece4 --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.progress.course.json @@ -0,0 +1,34 @@ +{ + "name": "openedx.completion_aggregator.progress.course", + "timestamp": "2023-12-05T21:34:52.909063+00:00", + "data": { + "user_id": 4, + "course_id": "course-v1:edX+DemoX+Demo_Course", + "context_key": "course-v1:edX+DemoX+Demo_Course", + "block_id": "block-v1:edX+DemoX+Demo_Course+type@course+block@course", + "block_type": "course", + "percent": 0.8, + "earned": 8, + "possible": 10 + }, + "context": { + "course_id": "course-v1:edX+DemoX+Demo_Course", + "course_user_tags": {}, + "session": "056aca2a1c6b76742b283e73d3424453", + "user_id": 3, + "username": "openedx", + "ip": "172.18.0.1", + "host": "localhost:18000", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", + "path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@problem+block@7c54b16c8ed34f9f8772015178c7a175/handler/xmodule_handler/problem_check", + "referer": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course+type@vertical+block@dd8110c941b94d929b56841195213797?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view", + "accept_language": "en-US,en;q=0.9,es;q=0.8", + "client_id": null, + "org_id": "edX", + "enterprise_uuid": "", + "module": { + "display_name": "Checkboxes", + "usage_key": "block-v1:edX+DemoX+Demo_Course+type@problem+block@7c54b16c8ed34f9f8772015178c7a175" + } + } +} diff --git a/tests/fixtures/raw/openedx.completion_aggregator.progress.sequential.json b/tests/fixtures/raw/openedx.completion_aggregator.progress.sequential.json new file mode 100644 index 00000000..949f44aa --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.progress.sequential.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.progress.sequential", + "timestamp": "2023-12-05T21:34:52.909063+00:00", + "data": { + "user_id": 4, + "course_id": "course-v1:edX+DemoX+Demo_Course", + "context_key": "course-v1:edX+DemoX+Demo_Course", + "block_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bf1eac10ebb649e3aaf9cc07325f8e04", + "block_type": "course", + "percent": 0.6, + "earned": 6, + "possible": 10 + }, + "context": { + "course_id": "course-v1:edX+DemoX+Demo_Course", + "course_user_tags": {}, + "session": "056aca2a1c6b76742b283e73d3424453", + "user_id": 3, + "username": "openedx", + "ip": "172.18.0.1", + "host": "localhost:18000", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", + "path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@problem+block@7c54b16c8ed34f9f8772015178c7a175/handler/xmodule_handler/problem_check", + "referer": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course+type@vertical+block@dd8110c941b94d929b56841195213797?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view", + "accept_language": "en-US,en;q=0.9,es;q=0.8", + "client_id": null, + "org_id": "edX", + "enterprise_uuid": "" + } +} diff --git a/tests/fixtures/raw/openedx.completion_aggregator.progress.vertical.json b/tests/fixtures/raw/openedx.completion_aggregator.progress.vertical.json new file mode 100644 index 00000000..3d8d8a49 --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.progress.vertical.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.progress.vertical", + "timestamp": "2023-12-05T21:34:52.909063+00:00", + "data": { + "user_id": 4, + "course_id": "course-v1:edX+DemoX+Demo_Course", + "context_key": "course-v1:edX+DemoX+Demo_Course", + "block_id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@e1fabd9fa55f441caa75580f258ffbc3", + "block_type": "course", + "percent": 1, + "earned": 10, + "possible": 10 + }, + "context": { + "course_id": "course-v1:edX+DemoX+Demo_Course", + "course_user_tags": {}, + "session": "056aca2a1c6b76742b283e73d3424453", + "user_id": 3, + "username": "openedx", + "ip": "172.18.0.1", + "host": "localhost:18000", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36", + "path": "/courses/course-v1:edX+DemoX+Demo_Course/xblock/block-v1:edX+DemoX+Demo_Course+type@problem+block@7c54b16c8ed34f9f8772015178c7a175/handler/xmodule_handler/problem_check", + "referer": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course+type@vertical+block@dd8110c941b94d929b56841195213797?show_title=0&show_bookmark_button=0&recheck_access=1&view=student_view", + "accept_language": "en-US,en;q=0.9,es;q=0.8", + "client_id": null, + "org_id": "edX", + "enterprise_uuid": "" + } +} diff --git a/tests/test_plugin_settings.py b/tests/test_plugin_settings.py index d3bf846f..443c3d98 100644 --- a/tests/test_plugin_settings.py +++ b/tests/test_plugin_settings.py @@ -1,10 +1,13 @@ """ Test the aggregator plugin settings. """ +from event_routing_backends.settings import common as erb_settings + from django.conf import settings from django.test import override_settings from completion_aggregator.settings import aws as aws_settings +from completion_aggregator.settings import common as common_settings @override_settings(ENV_TOKENS={}) @@ -15,3 +18,30 @@ def test_production_settings(): aws_settings.plugin_settings(settings) assert settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES == settings.COMPLETION_AGGREGATOR_BLOCK_TYPES + + +def test_event_tracking_backends(): + """ + Test that the completion aggregator events are whitelisted on the ERB backends. + """ + # Event Routing Backend settings must be loaded first. + erb_settings.plugin_settings(settings) + common_settings.plugin_settings(settings) + + transformer_options = settings.EVENT_TRACKING_BACKENDS['event_transformer']['OPTIONS'] + toplevel_whitelist = set(transformer_options['processors'][0]['OPTIONS']['whitelist']) + xapi_whitelist = set(transformer_options['backends']['xapi']['OPTIONS']['processors'][0]['OPTIONS']['whitelist']) + + assert toplevel_whitelist, "No whitelist found in event_transformer processors?" + assert xapi_whitelist, "No whitelist found in event_transformer processors?" + + expected_events = { + 'openedx.completion_aggregator.progress.course', + 'openedx.completion_aggregator.progress.chapter', + 'openedx.completion_aggregator.progress.sequential', + 'openedx.completion_aggregator.progress.vertical', + } + + # Ensure expected_events is a subset of these whitelists + assert expected_events < toplevel_whitelist, "Aggregator events not found in event_transformer whitelist" + assert expected_events < xapi_whitelist, "Aggregator events not found in xapi whitelist" diff --git a/tests/test_xapi.py b/tests/test_xapi.py new file mode 100644 index 00000000..7a425baf --- /dev/null +++ b/tests/test_xapi.py @@ -0,0 +1,75 @@ +""" +Test the completion aggregator transformers. +""" +import os +from unittest.mock import patch +from uuid import UUID + +import ddt +from event_routing_backends.processors.xapi.tests.test_transformers import XApiTransformersFixturesTestMixin +from event_routing_backends.settings import common as erb_settings + +from django.conf import settings +from django.test import TestCase + +from completion_aggregator.settings import common as common_settings + + +@ddt.ddt +class TestXApiTransformers(XApiTransformersFixturesTestMixin, TestCase): + """ + Test xApi event transforms and settings. + """ + TEST_DIR_PATH = os.path.dirname(os.path.abspath(__file__)) + + EVENT_FIXTURE_FILENAMES = [ + event_file_name for event_file_name in os.listdir( + f'{TEST_DIR_PATH}/fixtures/raw/' + ) if event_file_name.endswith(".json") + ] + + @property + def raw_events_fixture_path(self): + """ + Return the path to the expected transformed events fixture files. + """ + return f'{self.TEST_DIR_PATH}/fixtures/raw' + + @property + def expected_events_fixture_path(self): + """ + Return the path to the expected transformed events fixture files. + """ + return f'{self.TEST_DIR_PATH}/fixtures/expected' + + def setUp(self): + """ + Initialize the plugin settings. + """ + erb_settings.plugin_settings(settings) + common_settings.plugin_settings(settings) + + super().setUp() + + @patch('event_routing_backends.processors.xapi.transformer.get_anonymous_user_id') + @patch('event_routing_backends.processors.xapi.transformer.get_course_from_id') + @ddt.data(*EVENT_FIXTURE_FILENAMES) + def test_event_transformer(self, raw_event_file_path, mock_get_course_from_id, mock_get_anonymous_user_id): + # Generates the anonymized actor.name, + mock_get_anonymous_user_id.return_value = UUID('32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb') + + # Generates the contextActivities + mock_get_course_from_id.return_value = { + "display_name": "Demonstration Course", + "id": "course-v1:edX+DemoX+Demo_Course", + } + + # if an event's expected fixture doesn't exist, the test shouldn't fail. + # evaluate transformation of only supported event fixtures. + base_event_filename = os.path.basename(raw_event_file_path) + + expected_event_file_path = f'{self.expected_events_fixture_path}/{base_event_filename}' + + assert os.path.isfile(expected_event_file_path) + + self.check_event_transformer(raw_event_file_path, expected_event_file_path)