From ead8f61b1081a87aa7e37887d82724f807320129 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Mon, 4 Dec 2023 12:33:18 -0500 Subject: [PATCH 01/23] feat: add tracking logs --- completion_aggregator/models.py | 38 ++++++++++++++++++++++++ completion_aggregator/settings/common.py | 17 +++++++++++ requirements/base.in | 1 + requirements/base.txt | 4 ++- test_settings.py | 17 +++++++++++ tests/test_models.py | 37 +++++++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) diff --git a/completion_aggregator/models.py b/completion_aggregator/models.py index c1fde462..513ad6e8 100644 --- a/completion_aggregator/models.py +++ b/completion_aggregator/models.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from eventtracking import tracker from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField from opaque_keys.edx.keys import CourseKey, UsageKey @@ -171,8 +172,44 @@ def submit_completion(self, user, course_key, block_key, aggregation_name, earne 'last_modified': last_modified, }, ) + self.emit_completion_aggregator_logs([obj]) + return obj, is_new + @staticmethod + def emit_completion_aggregator_logs(updated_aggregators): + """ + Emit a tracking log for each element of the list parameter. + + Parameters + ---------- + updated_aggregators: List of Aggregator intances + + """ + for obj in updated_aggregators: + event = "progress" if obj.percent < 1 else "completion" + event_type = obj.aggregation_name + + if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, []): + continue + + event_name = f"edx.completion_aggregator.{event}.{event_type}" + + tracker.emit( + event_name, + { + "user_id": obj.user_id, + "course_id": str(obj.course_key), + "block_id": str(obj.block_key), + "modified": obj.modified, + "created": obj.created, + "earned": obj.earned, + "possible": obj.possible, + "percent": obj.percent, + "type": event_type, + } + ) + def bulk_create_or_update(self, updated_aggregators): """ Update the collection of aggregator object using mysql insert on duplicate update query. @@ -194,6 +231,7 @@ def bulk_create_or_update(self, updated_aggregators): else: aggregation_data = [obj.get_values() for obj in updated_aggregators] cur.executemany(INSERT_OR_UPDATE_AGGREGATOR_QUERY, aggregation_data) + self.emit_completion_aggregator_logs(updated_aggregators) class Aggregator(TimeStampedModel): diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 8d2aa479..7e648cb4 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -9,6 +9,23 @@ def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ + # Emit feature allows to publish two kind of events progress and completion + # This setting controls which type of event will be published to change the default behavior + # the block type should be removed or added from the progress or completion list. + settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { + "progress": [ + "course", + "chapter", + "sequential", + "vertical", + ], + "completion": [ + "course", + "chapter", + "sequential", + "vertical", + ] + } settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = { 'course', 'chapter', diff --git a/requirements/base.in b/requirements/base.in index 36fb18c6..8fee866b 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -9,5 +9,6 @@ django-model-utils # Provides TimeStampedModel abstract base class edx-opaque-keys # Provides CourseKey and UsageKey edx-completion edx-toggles +event-tracking six XBlock[django] diff --git a/requirements/base.txt b/requirements/base.txt index a3415760..5f13f786 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -113,7 +113,9 @@ edx-toggles==5.1.0 # -r requirements/base.in # edx-completion event-tracking==2.2.0 - # via edx-completion + # via + # -r requirements/base.in + # edx-completion fs==2.4.16 # via # fs-s3fs diff --git a/test_settings.py b/test_settings.py index bdd4004b..4bd61f7c 100644 --- a/test_settings.py +++ b/test_settings.py @@ -18,6 +18,20 @@ def root(*args): return join(abspath(dirname(__file__)), *args) +ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { + "progress": [ + "course", + "chapter", + "sequential", + "vertical", + ], + "completion": [ + "course", + "chapter", + "sequential", + "vertical", + ] +} AUTH_USER_MODEL = 'auth.User' CELERY_ALWAYS_EAGER = True COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential'} @@ -53,6 +67,7 @@ def root(*args): 'oauth2_provider', 'waffle', 'test_utils.test_app', + 'eventtracking.django.apps.EventTrackingConfig', ) LOCALE_PATHS = [root('completion_aggregator', 'conf', 'locale')] @@ -81,5 +96,7 @@ def root(*args): ] USE_TZ = True +EVENT_TRACKING_ENABLED = True + # pylint: disable=unused-import,wrong-import-position from test_utils.test_app import celery # isort:skip diff --git a/tests/test_models.py b/tests/test_models.py index e84a57c1..7ab1ce74 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -9,8 +9,10 @@ import ddt import pytest import six +from mock import patch from opaque_keys.edx.keys import UsageKey +from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.test import TestCase @@ -31,6 +33,12 @@ class AggregatorTestCase(TestCase): def setUp(self): super().setUp() self.user = get_user_model().objects.create(username='testuser') + self.tracker_patch = patch('completion_aggregator.models.tracker') + self.tracker_mock = self.tracker_patch.start() + + def tearDown(self): + """Stop patching.""" + self.tracker_mock.stop() def test_submit_completion_with_invalid_user(self): with pytest.raises(TypeError): @@ -43,6 +51,7 @@ def test_submit_completion_with_invalid_user(self): possible=27.0, last_modified=now(), ) + self.tracker_mock.assert_not_called() @ddt.data( # Valid arguments @@ -64,6 +73,7 @@ def test_submit_completion_with_valid_data(self, block_key_obj, aggregate_name, self.assertEqual(obj.earned, earned) self.assertEqual(obj.possible, possible) self.assertEqual(obj.percent, expected_percent) + self.assert_emit_method_called(obj) @ddt.data( # Earned greater than possible @@ -105,6 +115,7 @@ def test_submit_completion_with_exception( ) self.assertEqual(exception_message, str(context_manager.exception)) + self.tracker_mock.assert_not_called() @ddt.data( ( @@ -129,6 +140,7 @@ def test_aggregate_completion_string( f'{six.text_type(block_key_obj)}: {expected_percent}' ) self.assertEqual(six.text_type(obj), expected_string) + self.assert_emit_method_called(obj) @ddt.data( # Changes the value of earned. This does not create a new object. @@ -179,6 +191,7 @@ def test_submit_completion_twice_with_changes( ) self.assertEqual(obj.percent, expected_percent) self.assertTrue(is_new) + self.assert_emit_method_called(obj) new_obj, is_new = Aggregator.objects.submit_completion( user=self.user, @@ -193,6 +206,7 @@ def test_submit_completion_twice_with_changes( self.assertEqual(is_new, is_second_obj_new) if is_second_obj_new: self.assertNotEqual(obj.id, new_obj.id) + self.assert_emit_method_called(new_obj) @ddt.data( (BLOCK_KEY_OBJ, 'course', 0.5, 1, 0.5), @@ -211,3 +225,26 @@ def test_get_values(self, block_key_obj, aggregate_name, earned, possible, expec values = aggregator.get_values() self.assertEqual(values['user'], self.user.id) self.assertEqual(values['percent'], expected_percent) + + def assert_emit_method_called(self, obj): + """Verify that the tracker.emit method was called once with the right values.""" + if obj.aggregation_name not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES: + return + + event = "progress" if obj.percent < 1 else "completion" + + self.tracker_mock.emit.assert_called_once_with( + f"edx.completion_aggregator.{event}.{obj.aggregation_name}", + { + "user_id": obj.user_id, + "course_id": str(obj.course_key), + "block_id": str(obj.block_key), + "modified": obj.modified, + "created": obj.created, + "earned": obj.earned, + "possible": obj.possible, + "percent": obj.percent, + "type": obj.aggregation_name, + } + ) + self.tracker_mock.emit.reset_mock() From a82984ef75b448544804fea8675e16b8a2dd7d26 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 5 Dec 2023 12:43:46 -0500 Subject: [PATCH 02/23] feat: addressing pr comments --- completion_aggregator/models.py | 24 ++++++++++++------------ completion_aggregator/settings/common.py | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/completion_aggregator/models.py b/completion_aggregator/models.py index 513ad6e8..17a44d9e 100644 --- a/completion_aggregator/models.py +++ b/completion_aggregator/models.py @@ -186,11 +186,11 @@ def emit_completion_aggregator_logs(updated_aggregators): updated_aggregators: List of Aggregator intances """ - for obj in updated_aggregators: - event = "progress" if obj.percent < 1 else "completion" - event_type = obj.aggregation_name + for aggregator in updated_aggregators: + event = "progress" if aggregator.percent < 1 else "completion" + event_type = aggregator.aggregation_name - if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, []): + if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, {}): continue event_name = f"edx.completion_aggregator.{event}.{event_type}" @@ -198,14 +198,14 @@ def emit_completion_aggregator_logs(updated_aggregators): tracker.emit( event_name, { - "user_id": obj.user_id, - "course_id": str(obj.course_key), - "block_id": str(obj.block_key), - "modified": obj.modified, - "created": obj.created, - "earned": obj.earned, - "possible": obj.possible, - "percent": obj.percent, + "user_id": aggregator.user_id, + "course_id": str(aggregator.course_key), + "block_id": str(aggregator.block_key), + "modified": aggregator.modified, + "created": aggregator.created, + "earned": aggregator.earned, + "possible": aggregator.possible, + "percent": aggregator.percent, "type": event_type, } ) diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 7e648cb4..75466014 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -13,18 +13,18 @@ def plugin_settings(settings): # This setting controls which type of event will be published to change the default behavior # the block type should be removed or added from the progress or completion list. settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { - "progress": [ + "progress": { "course", "chapter", "sequential", "vertical", - ], - "completion": [ + }, + "completion": { "course", "chapter", "sequential", "vertical", - ] + } } settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = { 'course', From d71cffea414cb530dfccf366fc0a8890bc6af8e0 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 4 Jun 2024 08:09:29 +0930 Subject: [PATCH 03/23] chore: bump edx-completion --- requirements/base.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc.txt | 2 +- requirements/quality.txt | 2 +- requirements/test.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 61771d72..2744713d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -97,7 +97,7 @@ drf-jwt==1.19.2 # via edx-drf-extensions edx-celeryutils==1.2.5 # via -r requirements/base.in -edx-completion==4.4.0 +edx-completion==4.6.0 # via -r requirements/base.in edx-django-utils==5.10.1 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index f1ae81f4..b398bce8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -173,7 +173,7 @@ drf-jwt==1.19.2 # edx-drf-extensions edx-celeryutils==1.2.5 # via -r requirements/quality.txt -edx-completion==4.4.0 +edx-completion==4.6.0 # via -r requirements/quality.txt edx-django-utils==5.10.1 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index 660d2225..17b75bf1 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -184,7 +184,7 @@ edx-celeryutils==1.2.5 # via # -r requirements/base.txt # -r requirements/test.txt -edx-completion==4.4.0 +edx-completion==4.6.0 # via # -r requirements/base.txt # -r requirements/test.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 6701a826..4ef12726 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -158,7 +158,7 @@ drf-jwt==1.19.2 # edx-drf-extensions edx-celeryutils==1.2.5 # via -r requirements/test.txt -edx-completion==4.4.0 +edx-completion==4.6.0 # via -r requirements/test.txt edx-django-utils==5.10.1 # via diff --git a/requirements/test.txt b/requirements/test.txt index f695b48c..041bb5ae 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -143,7 +143,7 @@ drf-jwt==1.19.2 # edx-drf-extensions edx-celeryutils==1.2.5 # via -r requirements/base.txt -edx-completion==4.4.0 +edx-completion==4.6.0 # via -r requirements/base.txt edx-django-utils==5.10.1 # via From 407a1d1fb5488615c0719b3df57e8c8661e0b046 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 4 Jun 2024 08:13:13 +0930 Subject: [PATCH 04/23] fix: use "openedx" as the event prefix instead of "edx" --- completion_aggregator/models.py | 2 +- tests/test_models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/completion_aggregator/models.py b/completion_aggregator/models.py index 17a44d9e..cd0c9497 100644 --- a/completion_aggregator/models.py +++ b/completion_aggregator/models.py @@ -193,7 +193,7 @@ def emit_completion_aggregator_logs(updated_aggregators): if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, {}): continue - event_name = f"edx.completion_aggregator.{event}.{event_type}" + event_name = f"openedx.completion_aggregator.{event}.{event_type}" tracker.emit( event_name, diff --git a/tests/test_models.py b/tests/test_models.py index 7ab1ce74..3476d146 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -234,7 +234,7 @@ def assert_emit_method_called(self, obj): event = "progress" if obj.percent < 1 else "completion" self.tracker_mock.emit.assert_called_once_with( - f"edx.completion_aggregator.{event}.{obj.aggregation_name}", + f"openedx.completion_aggregator.{event}.{obj.aggregation_name}", { "user_id": obj.user_id, "course_id": str(obj.course_key), From 4a229fd9197924ed28c09e60b2395b20b3bdf0e5 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Wed, 12 Jun 2024 15:26:34 +0930 Subject: [PATCH 05/23] temp: adds edx-event-routing-backends WIP requirement use branch from https://github.com/openedx/event-routing-backends/pull/431 --- requirements/base.in | 1 + requirements/base.txt | 60 +++++++++++++++++++++++++--- requirements/ci.txt | 4 +- requirements/constraints.txt | 4 ++ requirements/dev.txt | 72 ++++++++++++++++++++++++++++++--- requirements/doc.txt | 77 ++++++++++++++++++++++++++++++++++-- requirements/pip-tools.txt | 6 ++- requirements/quality.txt | 69 ++++++++++++++++++++++++++++++-- requirements/test.txt | 74 +++++++++++++++++++++++++++++++--- test_settings.py | 3 ++ 10 files changed, 342 insertions(+), 28 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index 8fee866b..68b9bbbb 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -12,3 +12,4 @@ edx-toggles event-tracking 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..55ba8910 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 @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx + # 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/constraints.txt b/requirements/constraints.txt index 2c112072..5b938334 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -16,3 +16,7 @@ # https://github.com/openedx/completion/blob/v4.2.0/completion/__init__.py#L7 pytest<6.0.0 pluggy<1.0.0 + + +# TODO Temporary constraint to fix issues with using event-routing-backends +git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx#egg=edx_event_routing_backends diff --git a/requirements/dev.txt b/requirements/dev.txt index b398bce8..e18e60a1 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 @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx + # 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,21 @@ 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 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 +270,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 +296,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 +352,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 +467,7 @@ python-dateutil==2.8.2 # -r requirements/quality.txt # botocore # celery + # edx-event-routing-backends # freezegun # xblock python-slugify==8.0.4 @@ -429,7 +480,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -437,13 +490,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 +522,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -487,6 +545,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..8382dc9d 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 @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx + # 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,24 @@ 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 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 +297,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 +319,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 +381,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 +464,7 @@ python-dateutil==2.8.2 # -r requirements/test.txt # botocore # celery + # edx-event-routing-backends # freezegun # xblock python-slugify==8.0.4 @@ -422,7 +480,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -433,14 +493,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 +533,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -507,6 +573,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..6dcff4a4 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 @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx + # 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,21 @@ 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 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 +250,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 +271,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 +325,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 +415,7 @@ python-dateutil==2.8.2 # -r requirements/test.txt # botocore # celery + # edx-event-routing-backends # freezegun # xblock python-slugify==8.0.4 @@ -378,7 +428,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -386,13 +438,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 +469,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -432,6 +489,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.txt b/requirements/test.txt index 041bb5ae..fd026e3a 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 @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx + # 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,21 @@ 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 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 +235,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 +252,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 +300,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 +361,7 @@ python-dateutil==2.8.2 # -r requirements/base.txt # botocore # celery + # edx-event-routing-backends # freezegun # xblock python-slugify==8.0.4 @@ -322,7 +374,9 @@ pytz==2024.1 # django # djangorestframework # edx-completion + # edx-event-routing-backends # event-tracking + # tincan # xblock pyyaml==6.0.1 # via @@ -330,13 +384,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 +415,7 @@ six==1.16.0 # freezegun # fs # fs-s3fs + # isodate # mock # more-itertools # python-dateutil @@ -373,6 +433,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_settings.py b/test_settings.py index 4bd61f7c..2a79e590 100644 --- a/test_settings.py +++ b/test_settings.py @@ -68,6 +68,7 @@ def root(*args): 'waffle', 'test_utils.test_app', 'eventtracking.django.apps.EventTrackingConfig', + 'event_routing_backends', ) LOCALE_PATHS = [root('completion_aggregator', 'conf', 'locale')] @@ -97,6 +98,8 @@ def root(*args): USE_TZ = True EVENT_TRACKING_ENABLED = True +EVENT_TRACKING_BACKENDS = {} +LMS_ROOT_URL = "http://lms.url" # pylint: disable=unused-import,wrong-import-position from test_utils.test_app import celery # isort:skip From 7652429f326c1721cc9f07ef2256595ed593f8d7 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Wed, 12 Jun 2024 15:27:32 +0930 Subject: [PATCH 06/23] feat: adds xApiTransforms for completion aggregator events * adds dependency on edx-event-routing-backends * adds transformers to xapi.completion and xapi.progress * adds the completion_aggregator events to the event tracking whitelist --- completion_aggregator/apps.py | 6 +- completion_aggregator/settings/aws.py | 5 + completion_aggregator/settings/common.py | 16 +++ completion_aggregator/transformers.py | 121 +++++++++++++++++++++++ tests/test_plugin_settings.py | 39 ++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 tests/test_plugin_settings.py diff --git a/completion_aggregator/apps.py b/completion_aggregator/apps.py index 07595a66..53234c4b 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 transformers + from .tasks import aggregation_tasks, handler_tasks diff --git a/completion_aggregator/settings/aws.py b/completion_aggregator/settings/aws.py index 84852801..6a0ea5dd 100644 --- a/completion_aggregator/settings/aws.py +++ b/completion_aggregator/settings/aws.py @@ -4,11 +4,16 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from event_routing_backends.settings import production as erb_settings + def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ + # Load Event Routing Backend production settings first. + erb_settings.plugin_settings(settings) + settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = set(settings.ENV_TOKENS.get( 'COMPLETION_AGGREGATOR_BLOCK_TYPES', settings.COMPLETION_AGGREGATOR_BLOCK_TYPES, diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 75466014..8b015cfa 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): """ @@ -32,6 +34,7 @@ def plugin_settings(settings): 'sequential', 'vertical', } + settings.COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False # Names of the batch operations locks @@ -52,3 +55,16 @@ 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. + enabled_aggregator_events = [ + f'openedx.completion_aggregator.{event_type}.{block_type}' + + for event_type in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES + for block_type in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES[event_type] + ] + settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS += enabled_aggregator_events + settings.EVENT_TRACKING_BACKENDS.update(event_tracking_backends_config( + settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS, + settings.EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS, + )) diff --git a/completion_aggregator/transformers.py b/completion_aggregator/transformers.py index 0ba63667..3dcadc52 100644 --- a/completion_aggregator/transformers.py +++ b/completion_aggregator/transformers.py @@ -7,6 +7,11 @@ except ImportError: BlockStructureTransformer = object +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, LanguageMap, Result, Verb from xblock.completable import XBlockCompletionMode @@ -79,3 +84,119 @@ def transform(self, usage_info, block_structure): # pylint: disable=unused-argu if completion_mode != XBlockCompletionMode.EXCLUDED: aggregators = self.calculate_aggregators(block_structure, block_key) block_structure.set_transformer_block_field(block_key, self, self.AGGREGATORS, aggregators) + + +class BaseAggregatorXApiTransformer(XApiTransformer): + """ + Base transformer for all completion aggregator events. + """ + + object_type = None + + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + if not self.object_type: + raise NotImplementedError() + + return Activity( + id=self.get_object_iri("xblock", self.get_data("data.block_id")), + definition=ActivityDefinition( + type=self.object_type, + ), + ) + + +class BaseProgressTransformer(BaseAggregatorXApiTransformer): + """ + 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.base_progress.get_object", + ) + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + return super().get_object() + + def get_result(self) -> Result: + """ + Get result for xAPI transformed event. + """ + return Result( + completion=self.get_data("data.percent") == 1.0, + score={ + "scaled": self.get_data("data.percent") or 0 + } + ) + + +@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 + + +class BaseCompletionTransformer(BaseAggregatorXApiTransformer): + """ + Base transformer for aggregator completion events. + """ + + _verb = Verb( + id=constants.XAPI_VERB_COMPLETED, + display=LanguageMap({constants.EN: constants.COMPLETED}), + ) + object_type = None + + @openedx_filter( + filter_type="completion_aggregator.xapi.base_completion.get_object", + ) + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + return super().get_object() + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.chapter") +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.sequential") +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.vertical") +class ModuleCompletionTransformer(BaseCompletionTransformer): + """ + Transformer for events generated when a user completes a section, subsection or unit. + """ + + object_type = constants.XAPI_ACTIVITY_MODULE + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.course") +class CourseCompletionTransformer(BaseCompletionTransformer): + """ + Transformer for event generated when a user completes a course. + """ + + object_type = constants.XAPI_ACTIVITY_COURSE diff --git a/tests/test_plugin_settings.py b/tests/test_plugin_settings.py new file mode 100644 index 00000000..d3c0bb9a --- /dev/null +++ b/tests/test_plugin_settings.py @@ -0,0 +1,39 @@ +""" +Test the aggregator plugin settings. +""" +from event_routing_backends.settings import common as erb_settings + +from django.conf import settings + +from completion_aggregator.settings import common as common_settings + + +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', + 'openedx.completion_aggregator.completion.course', + 'openedx.completion_aggregator.completion.chapter', + 'openedx.completion_aggregator.completion.sequential', + 'openedx.completion_aggregator.completion.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" From bde2bdfc03a63d3a63f7d73eb56e0a03dd4eb9d7 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 15:51:08 +0930 Subject: [PATCH 07/23] chore: bumps version to 4.0.4 and adds CHANGELOG entry. --- CHANGELOG.rst | 6 ++++++ completion_aggregator/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5ec8ff4..2b626eb1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,12 @@ Change Log Unreleased ~~~~~~~~~~ +[4.0.4] - 2024-06-13 +~~~~~~~~~~~~~~~~~~~~ + +* Emit tracking log events for `openedx.completion_aggregator.progress.*` and + `openedx.completion_aggregator.completion.*` for the various block/course types + [4.0.3] - 2023-10-24 ~~~~~~~~~~~~~~~~~~~~ diff --git a/completion_aggregator/__init__.py b/completion_aggregator/__init__.py index a9a396aa..fcb5b98c 100644 --- a/completion_aggregator/__init__.py +++ b/completion_aggregator/__init__.py @@ -5,4 +5,4 @@ from __future__ import absolute_import, unicode_literals -__version__ = '4.0.3' +__version__ = '4.0.4' From abfdb2a5240f6130e5cf499757ccf1eb4e80c7c0 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 16:08:25 +0930 Subject: [PATCH 08/23] test: add call to plugin_settings to fix coverage --- tests/test_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 3476d146..359aa7d0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -18,6 +18,7 @@ from django.test import TestCase from django.utils.timezone import now +from completion_aggregator.settings import common as common_settings from completion_aggregator.models import Aggregator @@ -35,6 +36,7 @@ def setUp(self): self.user = get_user_model().objects.create(username='testuser') self.tracker_patch = patch('completion_aggregator.models.tracker') self.tracker_mock = self.tracker_patch.start() + common_settings.plugin_settings(settings) def tearDown(self): """Stop patching.""" From 381dbd77a7eb04acf42e82598c602fb4333f7f32 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 16:27:04 +0930 Subject: [PATCH 09/23] test: make COMPLETION_AGGREGATOR_ASYNC_AGGREGATION consistent between test settings and plugin settings. The previous commit resulted in the COMPLETION_AGGREGATOR_ASYNC_AGGREGATION flag being flipped for some tests -- it was True in test_settings, but False in plugin_settings() So for these tests, we're now overriding that setting as needed. --- test_settings.py | 2 +- tests/test_batch.py | 3 +++ tests/test_core.py | 5 ++++- tests/test_models.py | 2 +- tests/test_signals.py | 3 ++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test_settings.py b/test_settings.py index 4bd61f7c..6cd17713 100644 --- a/test_settings.py +++ b/test_settings.py @@ -35,7 +35,7 @@ def root(*args): AUTH_USER_MODEL = 'auth.User' CELERY_ALWAYS_EAGER = True COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential'} -COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = True +COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False COMPLETION_AGGREGATOR_AGGREGATION_LOCK = 'COMPLETION_AGGREGATOR_AGGREGATION_LOCK' COMPLETION_AGGREGATOR_CLEANUP_LOCK = 'COMPLETION_AGGREGATOR_CLEANUP_LOCK' COMPLETION_AGGREGATOR_AGGREGATION_LOCK_TIMEOUT_SECONDS = 1800 diff --git a/tests/test_batch.py b/tests/test_batch.py index d294a02a..f1f711cf 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -60,6 +60,7 @@ def test_synchronous_aggregation(mock_task, users): assert mock_task.call_count == 4 # Called once per created BlockCompletion +@override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @patch('completion_aggregator.tasks.aggregation_tasks.update_aggregators.apply_async') def test_with_multiple_batches(mock_task, users): course_key = CourseKey.from_string('course-v1:OpenCraft+Onboarding+2018') @@ -87,6 +88,7 @@ def test_with_multiple_batches(mock_task, users): } +@override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @patch('completion_aggregator.tasks.aggregation_tasks.update_aggregators.apply_async') def test_with_stale_completions(mock_task, users): course_key = CourseKey.from_string('course-v1:OpenCraft+Onboarding+2018') @@ -107,6 +109,7 @@ def test_with_stale_completions(mock_task, users): assert mock_task.call_count == 2 # Called once for each user +@override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @patch('completion_aggregator.tasks.aggregation_tasks.update_aggregators.apply_async') def test_with_full_course_stale_completion(mock_task, users): course_key = CourseKey.from_string('course-v1:OpenCraft+Onboarding+2018') diff --git a/tests/test_core.py b/tests/test_core.py index 3fead01d..3cc7cba1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,7 @@ from xblock.core import XBlock from django.contrib.auth import get_user_model -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils.timezone import now from completion.models import BlockCompletion @@ -282,6 +282,7 @@ def test_unmodified_course(self): ] ) + @override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @XBlock.register_temp_plugin(CourseBlock, 'course') @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') @XBlock.register_temp_plugin(HTMLBlock, 'html') @@ -319,6 +320,7 @@ def test_modified_course(self): ] ) + @override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @XBlock.register_temp_plugin(CourseBlock, 'course') @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') @XBlock.register_temp_plugin(HTMLBlock, 'html') @@ -356,6 +358,7 @@ def test_pass_changed_blocks_argument(self): ] ) + @override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @XBlock.register_temp_plugin(CourseBlock, 'course') @XBlock.register_temp_plugin(OtherAggBlock, 'chapter') @XBlock.register_temp_plugin(HTMLBlock, 'html') diff --git a/tests/test_models.py b/tests/test_models.py index 359aa7d0..b5e0f328 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -18,8 +18,8 @@ from django.test import TestCase from django.utils.timezone import now -from completion_aggregator.settings import common as common_settings from completion_aggregator.models import Aggregator +from completion_aggregator.settings import common as common_settings @ddt.ddt diff --git a/tests/test_signals.py b/tests/test_signals.py index 8f8b2797..e9c7b3a6 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -10,7 +10,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey from django.contrib.auth import get_user_model -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils.timezone import now from completion.models import BlockCompletion @@ -35,6 +35,7 @@ def setUp(self): user_model.objects.get_or_create(username='user2')[0], ] + @override_settings(COMPLETION_AGGREGATOR_ASYNC_AGGREGATION=True) @patch('completion_aggregator.tasks.aggregation_tasks.update_aggregators.apply_async') def test_basic(self, mock_task): course_key = CourseKey.from_string('edX/test/2018') From e82c49daa3d53925ea9c9c7e13979a001cdfc858 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 16:39:01 +0930 Subject: [PATCH 10/23] fix: call enabled_aggregator_events with a settings arg and rearrange INSTALLED_APPS to fix tests. --- completion_aggregator/settings/common.py | 1 + test_settings.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 8b015cfa..31181b4a 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -65,6 +65,7 @@ def plugin_settings(settings): ] 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/test_settings.py b/test_settings.py index 5c5a9e28..0e60b81e 100644 --- a/test_settings.py +++ b/test_settings.py @@ -62,13 +62,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')] @@ -99,6 +99,8 @@ def root(*args): EVENT_TRACKING_ENABLED = True EVENT_TRACKING_BACKENDS = {} +EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS = [] +EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS = [] LMS_ROOT_URL = "http://lms.url" # pylint: disable=unused-import,wrong-import-position From 4542458d5c029be853478edf4e04fca9ac6365c6 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 21:34:57 +0930 Subject: [PATCH 11/23] test: adds transformer tests Uses transforms fixture test utilities from ERB to ensure that raw aggregator events are transformed to xAPI as expected. * test_output/ -- used by ERB to write failed event transforms for debugging * adds 'factory' as a test requirement because ERB tests needs it. --- .gitignore | 1 + completion_aggregator/transformers.py | 2 +- requirements/dev.txt | 7 ++ requirements/doc.txt | 7 ++ requirements/quality.txt | 7 ++ requirements/test.in | 1 + requirements/test.txt | 5 ++ test_output/.gitkeep | 1 + test_settings.py | 3 +- ...pletion_aggregator.completion.chapter.json | 45 +++++++++++ ...mpletion_aggregator.completion.course.json | 45 +++++++++++ ...tion_aggregator.completion.sequential.json | 45 +++++++++++ ...letion_aggregator.completion.vertical.json | 45 +++++++++++ ...ompletion_aggregator.progress.chapter.json | 51 +++++++++++++ ...completion_aggregator.progress.course.json | 51 +++++++++++++ ...letion_aggregator.progress.sequential.json | 51 +++++++++++++ ...mpletion_aggregator.progress.vertical.json | 51 +++++++++++++ ...pletion_aggregator.completion.chapter.json | 30 ++++++++ ...mpletion_aggregator.completion.course.json | 34 +++++++++ ...tion_aggregator.completion.sequential.json | 30 ++++++++ ...letion_aggregator.completion.vertical.json | 30 ++++++++ ...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_transformers.py | 75 +++++++++++++++++++ 26 files changed, 739 insertions(+), 2 deletions(-) create mode 100644 test_output/.gitkeep create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.course.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json create mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json 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.completion.chapter.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.course.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json create mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.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_transformers.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/completion_aggregator/transformers.py b/completion_aggregator/transformers.py index 3dcadc52..7d51f9c6 100644 --- a/completion_aggregator/transformers.py +++ b/completion_aggregator/transformers.py @@ -98,7 +98,7 @@ def get_object(self) -> Activity: Get object for xAPI transformed event. """ if not self.object_type: - raise NotImplementedError() + raise NotImplementedError() # pragma: no cover return Activity( id=self.get_object_iri("xblock", self.get_data("data.block_id")), diff --git a/requirements/dev.txt b/requirements/dev.txt index e18e60a1..7fcd4df0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -241,6 +241,12 @@ event-tracking==2.4.0 # -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 @@ -468,6 +474,7 @@ python-dateutil==2.8.2 # botocore # celery # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 diff --git a/requirements/doc.txt b/requirements/doc.txt index 8382dc9d..e7d494f9 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -264,6 +264,12 @@ event-tracking==2.4.0 # -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 @@ -465,6 +471,7 @@ python-dateutil==2.8.2 # botocore # celery # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 diff --git a/requirements/quality.txt b/requirements/quality.txt index 6dcff4a4..92723dc9 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -226,6 +226,12 @@ event-tracking==2.4.0 # -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 @@ -416,6 +422,7 @@ python-dateutil==2.8.2 # botocore # celery # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 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 fd026e3a..62148a60 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -211,6 +211,10 @@ event-tracking==2.4.0 # -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 @@ -362,6 +366,7 @@ python-dateutil==2.8.2 # botocore # celery # edx-event-routing-backends + # faker # freezegun # xblock python-slugify==8.0.4 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 0e60b81e..63d75345 100644 --- a/test_settings.py +++ b/test_settings.py @@ -101,7 +101,8 @@ def root(*args): EVENT_TRACKING_BACKENDS = {} EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS = [] EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS = [] -LMS_ROOT_URL = "http://lms.url" +LMS_ROOT_URL = "http://localhost:18000" +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.completion.chapter.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json new file mode 100644 index 00000000..eeb4de61 --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json @@ -0,0 +1,45 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", + "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/completed", + "display":{ + "en":"completed" + } + }, + "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" + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json new file mode 100644 index 00000000..a6931c7a --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json @@ -0,0 +1,45 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", + "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/completed", + "display":{ + "en":"completed" + } + }, + "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" + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json new file mode 100644 index 00000000..d54194b9 --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json @@ -0,0 +1,45 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", + "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/completed", + "display":{ + "en":"completed" + } + }, + "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" + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json new file mode 100644 index 00000000..97cfccde --- /dev/null +++ b/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json @@ -0,0 +1,45 @@ +{ + "actor": { + "objectType": "Agent", + "account": { + "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "homePage": "http://localhost:18000" + } + }, + "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", + "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/completed", + "display":{ + "en":"completed" + } + }, + "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" + } + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} 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..4a575518 --- /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":{ + "score":{ + "scaled":0.5 + }, + "completion":false + }, + "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..e2f58dfe --- /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":{ + "score":{ + "scaled":0.8 + }, + "completion":false + }, + "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..fbe78f6b --- /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":{ + "score":{ + "scaled":0.6 + }, + "completion":false + }, + "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..c44a4d7c --- /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":{ + "score":{ + "scaled":1.0 + }, + "completion":true + }, + "timestamp":"2023-12-05T21:34:52.909063+00:00" +} diff --git a/tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json new file mode 100644 index 00000000..16a0b214 --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.completion.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": "edx", + "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.completion.course.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.course.json new file mode 100644 index 00000000..9be7737f --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.completion.course.json @@ -0,0 +1,34 @@ +{ + "name": "openedx.completion_aggregator.completion.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.completion.sequential.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json new file mode 100644 index 00000000..ba4ec543 --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.completion.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.completion.vertical.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.vertical.json new file mode 100644 index 00000000..e179db5e --- /dev/null +++ b/tests/fixtures/raw/openedx.completion_aggregator.completion.vertical.json @@ -0,0 +1,30 @@ +{ + "name": "openedx.completion_aggregator.completion.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/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_transformers.py b/tests/test_transformers.py new file mode 100644 index 00000000..7a425baf --- /dev/null +++ b/tests/test_transformers.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) From a1ea151522c462967903ccf90d25a767200a8bd4 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 13 Jun 2024 22:12:21 +0930 Subject: [PATCH 12/23] revert: no need to change production settings --- completion_aggregator/settings/aws.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/completion_aggregator/settings/aws.py b/completion_aggregator/settings/aws.py index 6a0ea5dd..84852801 100644 --- a/completion_aggregator/settings/aws.py +++ b/completion_aggregator/settings/aws.py @@ -4,16 +4,11 @@ from __future__ import absolute_import, division, print_function, unicode_literals -from event_routing_backends.settings import production as erb_settings - def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ - # Load Event Routing Backend production settings first. - erb_settings.plugin_settings(settings) - settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = set(settings.ENV_TOKENS.get( 'COMPLETION_AGGREGATOR_BLOCK_TYPES', settings.COMPLETION_AGGREGATOR_BLOCK_TYPES, From 4d7d0970831b7d9d2f5130e181939d19aa4a9cd4 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 08:45:32 +0930 Subject: [PATCH 13/23] fix: address PR review --- README.rst | 9 +++++++++ completion_aggregator/__init__.py | 2 +- completion_aggregator/models.py | 5 ++++- completion_aggregator/settings/aws.py | 5 +++++ requirements/base.in | 2 +- test_settings.py | 11 ++++++----- tests/test_models.py | 17 ++++++++++++++++- 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 3ff7ae85..ca453fa9 100644 --- a/README.rst +++ b/README.rst @@ -47,6 +47,15 @@ API Details For details about how the completion aggregator's REST APIs can be used, please refer to `the docstrings in views.py `_. +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. + +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. + +You can limit which aggregator events are emitted by modifying the `ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES` setting to limit what event types are emitted (`progress` or `completion` or both), and what block types are emitted (`course`, `chapter`, `sequential`, `vertical`). To disable sending any completion aggregator tracking events, set `ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = None`. + Installation and Configuration ------------------------------ diff --git a/completion_aggregator/__init__.py b/completion_aggregator/__init__.py index fcb5b98c..a394c7f7 100644 --- a/completion_aggregator/__init__.py +++ b/completion_aggregator/__init__.py @@ -5,4 +5,4 @@ from __future__ import absolute_import, unicode_literals -__version__ = '4.0.4' +__version__ = '4.1.0' diff --git a/completion_aggregator/models.py b/completion_aggregator/models.py index cd0c9497..b112a4a7 100644 --- a/completion_aggregator/models.py +++ b/completion_aggregator/models.py @@ -183,9 +183,12 @@ def emit_completion_aggregator_logs(updated_aggregators): Parameters ---------- - updated_aggregators: List of Aggregator intances + updated_aggregators: List of Aggregator instances """ + if not settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES: + return + for aggregator in updated_aggregators: event = "progress" if aggregator.percent < 1 else "completion" event_type = aggregator.aggregation_name diff --git a/completion_aggregator/settings/aws.py b/completion_aggregator/settings/aws.py index 84852801..52ca8b26 100644 --- a/completion_aggregator/settings/aws.py +++ b/completion_aggregator/settings/aws.py @@ -9,6 +9,11 @@ def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ + settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = settings.ENV_TOKENS.get( + 'ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES', + settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES, + ) + settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = set(settings.ENV_TOKENS.get( 'COMPLETION_AGGREGATOR_BLOCK_TYPES', settings.COMPLETION_AGGREGATOR_BLOCK_TYPES, diff --git a/requirements/base.in b/requirements/base.in index 8fee866b..8355867e 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -9,6 +9,6 @@ django-model-utils # Provides TimeStampedModel abstract base class edx-opaque-keys # Provides CourseKey and UsageKey edx-completion edx-toggles -event-tracking +event-tracking # Allows the aggregator to emit tracking events six XBlock[django] diff --git a/test_settings.py b/test_settings.py index 6cd17713..97289424 100644 --- a/test_settings.py +++ b/test_settings.py @@ -19,22 +19,22 @@ def root(*args): ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { - "progress": [ + "progress": { "course", "chapter", "sequential", "vertical", - ], - "completion": [ + }, + "completion": { "course", "chapter", "sequential", "vertical", - ] + } } AUTH_USER_MODEL = 'auth.User' CELERY_ALWAYS_EAGER = True -COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential'} +COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential', 'vertical'} COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False COMPLETION_AGGREGATOR_AGGREGATION_LOCK = 'COMPLETION_AGGREGATOR_AGGREGATION_LOCK' COMPLETION_AGGREGATOR_CLEANUP_LOCK = 'COMPLETION_AGGREGATOR_CLEANUP_LOCK' @@ -96,6 +96,7 @@ def root(*args): ] USE_TZ = True +# Enables event tracking in the tests, see https://github.com/openedx/event-tracking EVENT_TRACKING_ENABLED = True # pylint: disable=unused-import,wrong-import-position diff --git a/tests/test_models.py b/tests/test_models.py index b5e0f328..a1226214 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -15,7 +15,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError -from django.test import TestCase +from django.test import TestCase, override_settings from django.utils.timezone import now from completion_aggregator.models import Aggregator @@ -228,6 +228,21 @@ def test_get_values(self, block_key_obj, aggregate_name, earned, possible, expec self.assertEqual(values['user'], self.user.id) self.assertEqual(values['percent'], expected_percent) + @override_settings(ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES=None) + def test_submit_completion_with_tracking_disabled(self): + _obj, is_new = Aggregator.objects.submit_completion( + user=self.user, + course_key=self.BLOCK_KEY_OBJ.course_key, + block_key=self.BLOCK_KEY_OBJ, + aggregation_name='course', + earned=0.5, + possible=1, + last_modified=now(), + ) + self.assertTrue(is_new) + self.assertEqual(len(Aggregator.objects.all()), 1) + self.tracker_mock.emit.assert_not_called() + def assert_emit_method_called(self, obj): """Verify that the tracker.emit method was called once with the right values.""" if obj.aggregation_name not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES: From b862bbce1637ec1520e0631009dce64bd41c8175 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 08:46:58 +0930 Subject: [PATCH 14/23] fix: CHANGELOG use minor version bump --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b626eb1..6e726aa0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,7 @@ Change Log Unreleased ~~~~~~~~~~ -[4.0.4] - 2024-06-13 +[4.1.0] - 2024-06-18 ~~~~~~~~~~~~~~~~~~~~ * Emit tracking log events for `openedx.completion_aggregator.progress.*` and From ab96203d5612c7455fc3bbd244cb5e2e84d0acf7 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 09:01:55 +0930 Subject: [PATCH 15/23] test: adds test for plugin settings --- tests/test_plugin_settings.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_plugin_settings.py diff --git a/tests/test_plugin_settings.py b/tests/test_plugin_settings.py new file mode 100644 index 00000000..d3c71ffa --- /dev/null +++ b/tests/test_plugin_settings.py @@ -0,0 +1,17 @@ +""" +Test the aggregator plugin settings. +""" +from django.conf import settings +from django.test import override_settings + +from completion_aggregator.settings import aws as aws_settings + + +@override_settings(ENV_TOKENS={}) +def test_production_settings(): + """ + Test that the completion aggregator production settings behave as expected + """ + aws_settings.plugin_settings(settings) + + assert list(settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.keys()) == ["progress", "completion"] From e694f1683094b43264f1357f82cf03635398dbed Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 09:58:08 +0930 Subject: [PATCH 16/23] refactor: moves the xAPI transforms to their own module --- completion_aggregator/apps.py | 2 +- completion_aggregator/transformers.py | 121 ------------------ completion_aggregator/xapi.py | 125 +++++++++++++++++++ tests/{test_transformers.py => test_xapi.py} | 0 4 files changed, 126 insertions(+), 122 deletions(-) create mode 100644 completion_aggregator/xapi.py rename tests/{test_transformers.py => test_xapi.py} (100%) diff --git a/completion_aggregator/apps.py b/completion_aggregator/apps.py index 53234c4b..ca58b5a9 100644 --- a/completion_aggregator/apps.py +++ b/completion_aggregator/apps.py @@ -43,5 +43,5 @@ def ready(self): signals.register() # pylint: disable=unused-import - from . import transformers + from . import xapi from .tasks import aggregation_tasks, handler_tasks diff --git a/completion_aggregator/transformers.py b/completion_aggregator/transformers.py index 7d51f9c6..0ba63667 100644 --- a/completion_aggregator/transformers.py +++ b/completion_aggregator/transformers.py @@ -7,11 +7,6 @@ except ImportError: BlockStructureTransformer = object -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, LanguageMap, Result, Verb from xblock.completable import XBlockCompletionMode @@ -84,119 +79,3 @@ def transform(self, usage_info, block_structure): # pylint: disable=unused-argu if completion_mode != XBlockCompletionMode.EXCLUDED: aggregators = self.calculate_aggregators(block_structure, block_key) block_structure.set_transformer_block_field(block_key, self, self.AGGREGATORS, aggregators) - - -class BaseAggregatorXApiTransformer(XApiTransformer): - """ - Base transformer for all completion aggregator events. - """ - - object_type = None - - 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, - ), - ) - - -class BaseProgressTransformer(BaseAggregatorXApiTransformer): - """ - 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.base_progress.get_object", - ) - def get_object(self) -> Activity: - """ - Get object for xAPI transformed event. - """ - return super().get_object() - - def get_result(self) -> Result: - """ - Get result for xAPI transformed event. - """ - return Result( - completion=self.get_data("data.percent") == 1.0, - score={ - "scaled": self.get_data("data.percent") or 0 - } - ) - - -@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 - - -class BaseCompletionTransformer(BaseAggregatorXApiTransformer): - """ - Base transformer for aggregator completion events. - """ - - _verb = Verb( - id=constants.XAPI_VERB_COMPLETED, - display=LanguageMap({constants.EN: constants.COMPLETED}), - ) - object_type = None - - @openedx_filter( - filter_type="completion_aggregator.xapi.base_completion.get_object", - ) - def get_object(self) -> Activity: - """ - Get object for xAPI transformed event. - """ - return super().get_object() - - -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.chapter") -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.sequential") -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.vertical") -class ModuleCompletionTransformer(BaseCompletionTransformer): - """ - Transformer for events generated when a user completes a section, subsection or unit. - """ - - object_type = constants.XAPI_ACTIVITY_MODULE - - -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.course") -class CourseCompletionTransformer(BaseCompletionTransformer): - """ - Transformer for event generated when a user completes a course. - """ - - object_type = constants.XAPI_ACTIVITY_COURSE diff --git a/completion_aggregator/xapi.py b/completion_aggregator/xapi.py new file mode 100644 index 00000000..5c63cab8 --- /dev/null +++ b/completion_aggregator/xapi.py @@ -0,0 +1,125 @@ +""" +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, LanguageMap, Result, Verb + + +class BaseAggregatorXApiTransformer(XApiTransformer): + """ + Base transformer for all completion aggregator events. + """ + + object_type = None + + 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, + ), + ) + + +class BaseProgressTransformer(BaseAggregatorXApiTransformer): + """ + 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.base_progress.get_object", + ) + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + return super().get_object() + + def get_result(self) -> Result: + """ + Get result for xAPI transformed event. + """ + return Result( + completion=self.get_data("data.percent") == 1.0, + score={ + "scaled": self.get_data("data.percent") or 0 + } + ) + + +@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 + + +class BaseCompletionTransformer(BaseAggregatorXApiTransformer): + """ + Base transformer for aggregator completion events. + """ + + _verb = Verb( + id=constants.XAPI_VERB_COMPLETED, + display=LanguageMap({constants.EN: constants.COMPLETED}), + ) + object_type = None + + @openedx_filter( + filter_type="completion_aggregator.xapi.base_completion.get_object", + ) + def get_object(self) -> Activity: + """ + Get object for xAPI transformed event. + """ + return super().get_object() + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.chapter") +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.sequential") +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.vertical") +class ModuleCompletionTransformer(BaseCompletionTransformer): + """ + Transformer for events generated when a user completes a section, subsection or unit. + """ + + object_type = constants.XAPI_ACTIVITY_MODULE + + +@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.course") +class CourseCompletionTransformer(BaseCompletionTransformer): + """ + Transformer for event generated when a user completes a course. + """ + + object_type = constants.XAPI_ACTIVITY_COURSE diff --git a/tests/test_transformers.py b/tests/test_xapi.py similarity index 100% rename from tests/test_transformers.py rename to tests/test_xapi.py From 1204f911f1ace5ae5a39db3008c6f2a76469503d Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 09:58:41 +0930 Subject: [PATCH 17/23] fix: use Result.extension to send progress % not Score.scaled --- completion_aggregator/xapi.py | 8 ++++---- test_settings.py | 2 ++ .../openedx.completion_aggregator.progress.chapter.json | 8 ++++---- .../openedx.completion_aggregator.progress.course.json | 8 ++++---- ...openedx.completion_aggregator.progress.sequential.json | 8 ++++---- .../openedx.completion_aggregator.progress.vertical.json | 8 ++++---- 6 files changed, 22 insertions(+), 20 deletions(-) diff --git a/completion_aggregator/xapi.py b/completion_aggregator/xapi.py index 5c63cab8..7c8e1d7e 100644 --- a/completion_aggregator/xapi.py +++ b/completion_aggregator/xapi.py @@ -6,7 +6,7 @@ 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, LanguageMap, Result, Verb +from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Result, Verb class BaseAggregatorXApiTransformer(XApiTransformer): @@ -58,9 +58,9 @@ def get_result(self) -> Result: """ return Result( completion=self.get_data("data.percent") == 1.0, - score={ - "scaled": self.get_data("data.percent") or 0 - } + extensions=Extensions({ + "progress": self.get_data("data.percent") or 0, + }), ) diff --git a/test_settings.py b/test_settings.py index 322d2d60..4875c662 100644 --- a/test_settings.py +++ b/test_settings.py @@ -102,7 +102,9 @@ def root(*args): EVENT_TRACKING_BACKENDS = {} EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS = [] EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS = [] +# 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 diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json index 4a575518..111d3903 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json @@ -42,10 +42,10 @@ } }, "result":{ - "score":{ - "scaled":0.5 - }, - "completion":false + "completion":false, + "extensions": { + "progress":0.5 + } }, "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 index e2f58dfe..965d5688 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json @@ -42,10 +42,10 @@ } }, "result":{ - "score":{ - "scaled":0.8 - }, - "completion":false + "completion":false, + "extensions": { + "progress":0.8 + } }, "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 index fbe78f6b..d937ed29 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json @@ -42,10 +42,10 @@ } }, "result":{ - "score":{ - "scaled":0.6 - }, - "completion":false + "completion":false, + "extensions": { + "progress":0.6 + } }, "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 index c44a4d7c..1e33b992 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json @@ -42,10 +42,10 @@ } }, "result":{ - "score":{ - "scaled":1.0 - }, - "completion":true + "completion":true, + "extensions": { + "progress":1.0 + } }, "timestamp":"2023-12-05T21:34:52.909063+00:00" } From 29f6b89955d42081a0cdc0bc47057695dcfebf91 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 12:19:30 +0930 Subject: [PATCH 18/23] refactor: simplify emitted events Ordinary completion events are simply "progress" events with a recorded progress %, so we should do the same here -- no need for "completed" events. --- CHANGELOG.rst | 4 ++-- README.rst | 2 +- completion_aggregator/models.py | 11 +++++------ completion_aggregator/settings/aws.py | 6 +++--- completion_aggregator/settings/common.py | 24 +++++++----------------- test_settings.py | 17 ++--------------- tests/test_models.py | 8 ++------ tests/test_plugin_settings.py | 2 +- 8 files changed, 23 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6e726aa0..70c28815 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,8 +17,8 @@ Unreleased [4.1.0] - 2024-06-18 ~~~~~~~~~~~~~~~~~~~~ -* Emit tracking log events for `openedx.completion_aggregator.progress.*` and - `openedx.completion_aggregator.completion.*` for the various block/course types +* Emit `openedx.completion_aggregator.progress.*` tracking log events for the + various block/course types [4.0.3] - 2023-10-24 ~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index ca453fa9..52272818 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ Like other parts of Open edX, the completion aggregator emits "tracking logs" ev 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. -You can limit which aggregator events are emitted by modifying the `ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES` setting to limit what event types are emitted (`progress` or `completion` or both), and what block types are emitted (`course`, `chapter`, `sequential`, `vertical`). To disable sending any completion aggregator tracking events, set `ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = None`. +You can limit which aggregator events are emitted by modifying the `COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES` setting to limit which block types (`course`, `chapter`, `sequential`, `vertical`) cause tracking events to be emitted. To disable sending any completion aggregator tracking events, set `COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES = None`. Installation and Configuration ------------------------------ diff --git a/completion_aggregator/models.py b/completion_aggregator/models.py index b112a4a7..24939ef7 100644 --- a/completion_aggregator/models.py +++ b/completion_aggregator/models.py @@ -186,17 +186,16 @@ def emit_completion_aggregator_logs(updated_aggregators): updated_aggregators: List of Aggregator instances """ - if not settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES: + if not settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES: return for aggregator in updated_aggregators: - event = "progress" if aggregator.percent < 1 else "completion" - event_type = aggregator.aggregation_name + block_type = aggregator.aggregation_name - if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, {}): + if block_type not in settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES: continue - event_name = f"openedx.completion_aggregator.{event}.{event_type}" + event_name = f"openedx.completion_aggregator.progress.{block_type}" tracker.emit( event_name, @@ -209,7 +208,7 @@ def emit_completion_aggregator_logs(updated_aggregators): "earned": aggregator.earned, "possible": aggregator.possible, "percent": aggregator.percent, - "type": event_type, + "type": block_type, } ) diff --git a/completion_aggregator/settings/aws.py b/completion_aggregator/settings/aws.py index 52ca8b26..22579eee 100644 --- a/completion_aggregator/settings/aws.py +++ b/completion_aggregator/settings/aws.py @@ -9,9 +9,9 @@ def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ - settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = settings.ENV_TOKENS.get( - 'ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES', - settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES, + settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES = settings.ENV_TOKENS.get( + 'COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES', + settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES, ) settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = set(settings.ENV_TOKENS.get( diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 75466014..271c8110 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -9,29 +9,19 @@ def plugin_settings(settings): """ Modify the provided settings object with settings specific to this plugin. """ - # Emit feature allows to publish two kind of events progress and completion - # This setting controls which type of event will be published to change the default behavior - # the block type should be removed or added from the progress or completion list. - settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { - "progress": { - "course", - "chapter", - "sequential", - "vertical", - }, - "completion": { - "course", - "chapter", - "sequential", - "vertical", - } - } settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = { 'course', 'chapter', 'sequential', 'vertical', } + + # Emit feature publishes progress events to track aggregated completion. + # Defaults to the full set of block types subject to completion aggregation. + # Block types may be removed from this list to limit the tracking log events emitted. + settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES = settings.COMPLETION_AGGREGATOR_BLOCK_TYPES + + # Synchronous completion aggregation is enabled by default settings.COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False # Names of the batch operations locks diff --git a/test_settings.py b/test_settings.py index 97289424..230bdad2 100644 --- a/test_settings.py +++ b/test_settings.py @@ -18,24 +18,11 @@ def root(*args): return join(abspath(dirname(__file__)), *args) -ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = { - "progress": { - "course", - "chapter", - "sequential", - "vertical", - }, - "completion": { - "course", - "chapter", - "sequential", - "vertical", - } -} AUTH_USER_MODEL = 'auth.User' CELERY_ALWAYS_EAGER = True -COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential', 'vertical'} COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False +COMPLETION_AGGREGATOR_BLOCK_TYPES = {'course', 'chapter', 'sequential', 'vertical'} +COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES = COMPLETION_AGGREGATOR_BLOCK_TYPES COMPLETION_AGGREGATOR_AGGREGATION_LOCK = 'COMPLETION_AGGREGATOR_AGGREGATION_LOCK' COMPLETION_AGGREGATOR_CLEANUP_LOCK = 'COMPLETION_AGGREGATOR_CLEANUP_LOCK' COMPLETION_AGGREGATOR_AGGREGATION_LOCK_TIMEOUT_SECONDS = 1800 diff --git a/tests/test_models.py b/tests/test_models.py index a1226214..61501449 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -228,7 +228,7 @@ def test_get_values(self, block_key_obj, aggregate_name, earned, possible, expec self.assertEqual(values['user'], self.user.id) self.assertEqual(values['percent'], expected_percent) - @override_settings(ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES=None) + @override_settings(COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES=None) def test_submit_completion_with_tracking_disabled(self): _obj, is_new = Aggregator.objects.submit_completion( user=self.user, @@ -245,13 +245,9 @@ def test_submit_completion_with_tracking_disabled(self): def assert_emit_method_called(self, obj): """Verify that the tracker.emit method was called once with the right values.""" - if obj.aggregation_name not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES: - return - - event = "progress" if obj.percent < 1 else "completion" self.tracker_mock.emit.assert_called_once_with( - f"openedx.completion_aggregator.{event}.{obj.aggregation_name}", + f"openedx.completion_aggregator.progress.{obj.aggregation_name}", { "user_id": obj.user_id, "course_id": str(obj.course_key), diff --git a/tests/test_plugin_settings.py b/tests/test_plugin_settings.py index d3c71ffa..d3bf846f 100644 --- a/tests/test_plugin_settings.py +++ b/tests/test_plugin_settings.py @@ -14,4 +14,4 @@ def test_production_settings(): """ aws_settings.plugin_settings(settings) - assert list(settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.keys()) == ["progress", "completion"] + assert settings.COMPLETION_AGGREGATOR_TRACKING_EVENT_TYPES == settings.COMPLETION_AGGREGATOR_BLOCK_TYPES From a142ce2d5182abcf5944fa3ceb9886c7430adce8 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 14 Jun 2024 12:36:05 +0930 Subject: [PATCH 19/23] refactor: only need to transform progress events --- completion_aggregator/xapi.py | 80 +++---------------- ...pletion_aggregator.completion.chapter.json | 45 ----------- ...mpletion_aggregator.completion.course.json | 45 ----------- ...tion_aggregator.completion.sequential.json | 45 ----------- ...letion_aggregator.completion.vertical.json | 45 ----------- ...ompletion_aggregator.progress.chapter.json | 2 +- ...completion_aggregator.progress.course.json | 2 +- ...letion_aggregator.progress.sequential.json | 2 +- ...mpletion_aggregator.progress.vertical.json | 2 +- ...pletion_aggregator.completion.chapter.json | 30 ------- ...mpletion_aggregator.completion.course.json | 34 -------- ...tion_aggregator.completion.sequential.json | 30 ------- ...letion_aggregator.completion.vertical.json | 30 ------- 13 files changed, 17 insertions(+), 375 deletions(-) delete mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json delete mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.course.json delete mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json delete mode 100644 tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json delete mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json delete mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.course.json delete mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json delete mode 100644 tests/fixtures/raw/openedx.completion_aggregator.completion.vertical.json diff --git a/completion_aggregator/xapi.py b/completion_aggregator/xapi.py index 7c8e1d7e..99992514 100644 --- a/completion_aggregator/xapi.py +++ b/completion_aggregator/xapi.py @@ -9,13 +9,21 @@ from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Result, Verb -class BaseAggregatorXApiTransformer(XApiTransformer): +class BaseProgressTransformer(XApiTransformer): """ - Base transformer for all completion aggregator events. + 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. @@ -30,36 +38,15 @@ def get_object(self) -> Activity: ), ) - -class BaseProgressTransformer(BaseAggregatorXApiTransformer): - """ - 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.base_progress.get_object", - ) - def get_object(self) -> Activity: - """ - Get object for xAPI transformed event. - """ - return super().get_object() - def get_result(self) -> Result: """ Get result for xAPI transformed event. """ + progress = self.get_data("data.percent") or 0 return Result( - completion=self.get_data("data.percent") == 1.0, + completion=progress == 1.0, extensions=Extensions({ - "progress": self.get_data("data.percent") or 0, + constants.XAPI_ACTIVITY_PROGRESS: (progress * 100), }), ) @@ -82,44 +69,3 @@ class CourseProgressTransformer(BaseProgressTransformer): """ object_type = constants.XAPI_ACTIVITY_COURSE - - -class BaseCompletionTransformer(BaseAggregatorXApiTransformer): - """ - Base transformer for aggregator completion events. - """ - - _verb = Verb( - id=constants.XAPI_VERB_COMPLETED, - display=LanguageMap({constants.EN: constants.COMPLETED}), - ) - object_type = None - - @openedx_filter( - filter_type="completion_aggregator.xapi.base_completion.get_object", - ) - def get_object(self) -> Activity: - """ - Get object for xAPI transformed event. - """ - return super().get_object() - - -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.chapter") -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.sequential") -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.vertical") -class ModuleCompletionTransformer(BaseCompletionTransformer): - """ - Transformer for events generated when a user completes a section, subsection or unit. - """ - - object_type = constants.XAPI_ACTIVITY_MODULE - - -@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.course") -class CourseCompletionTransformer(BaseCompletionTransformer): - """ - Transformer for event generated when a user completes a course. - """ - - object_type = constants.XAPI_ACTIVITY_COURSE diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json deleted file mode 100644 index eeb4de61..00000000 --- a/tests/fixtures/expected/openedx.completion_aggregator.completion.chapter.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "actor": { - "objectType": "Agent", - "account": { - "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", - "homePage": "http://localhost:18000" - } - }, - "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", - "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/completed", - "display":{ - "en":"completed" - } - }, - "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" - } - }, - "timestamp":"2023-12-05T21:34:52.909063+00:00" -} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json deleted file mode 100644 index a6931c7a..00000000 --- a/tests/fixtures/expected/openedx.completion_aggregator.completion.course.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "actor": { - "objectType": "Agent", - "account": { - "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", - "homePage": "http://localhost:18000" - } - }, - "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", - "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/completed", - "display":{ - "en":"completed" - } - }, - "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" - } - }, - "timestamp":"2023-12-05T21:34:52.909063+00:00" -} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json deleted file mode 100644 index d54194b9..00000000 --- a/tests/fixtures/expected/openedx.completion_aggregator.completion.sequential.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "actor": { - "objectType": "Agent", - "account": { - "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", - "homePage": "http://localhost:18000" - } - }, - "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", - "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/completed", - "display":{ - "en":"completed" - } - }, - "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" - } - }, - "timestamp":"2023-12-05T21:34:52.909063+00:00" -} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json b/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json deleted file mode 100644 index 97cfccde..00000000 --- a/tests/fixtures/expected/openedx.completion_aggregator.completion.vertical.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "actor": { - "objectType": "Agent", - "account": { - "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", - "homePage": "http://localhost:18000" - } - }, - "id":"484fe8d7-7a5b-52ff-a0ab-3d3d8c1a8b27", - "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/completed", - "display":{ - "en":"completed" - } - }, - "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" - } - }, - "timestamp":"2023-12-05T21:34:52.909063+00:00" -} diff --git a/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json index 111d3903..f8a8cb8f 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.chapter.json @@ -44,7 +44,7 @@ "result":{ "completion":false, "extensions": { - "progress":0.5 + "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 index 965d5688..2b089dc5 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.course.json @@ -44,7 +44,7 @@ "result":{ "completion":false, "extensions": { - "progress":0.8 + "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 index d937ed29..6a6aa381 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.sequential.json @@ -44,7 +44,7 @@ "result":{ "completion":false, "extensions": { - "progress":0.6 + "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 index 1e33b992..b2692614 100644 --- a/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json +++ b/tests/fixtures/expected/openedx.completion_aggregator.progress.vertical.json @@ -44,7 +44,7 @@ "result":{ "completion":true, "extensions": { - "progress":1.0 + "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.completion.chapter.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json deleted file mode 100644 index 16a0b214..00000000 --- a/tests/fixtures/raw/openedx.completion_aggregator.completion.chapter.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "openedx.completion_aggregator.completion.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": "edx", - "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.completion.course.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.course.json deleted file mode 100644 index 9be7737f..00000000 --- a/tests/fixtures/raw/openedx.completion_aggregator.completion.course.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "openedx.completion_aggregator.completion.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.completion.sequential.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json deleted file mode 100644 index ba4ec543..00000000 --- a/tests/fixtures/raw/openedx.completion_aggregator.completion.sequential.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "openedx.completion_aggregator.completion.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.completion.vertical.json b/tests/fixtures/raw/openedx.completion_aggregator.completion.vertical.json deleted file mode 100644 index e179db5e..00000000 --- a/tests/fixtures/raw/openedx.completion_aggregator.completion.vertical.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "openedx.completion_aggregator.completion.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": "" - } -} From 3ff8331fa7ace76a114276781d9391fd700b536c Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jun 2024 15:28:52 +0930 Subject: [PATCH 20/23] fix: ensure plugin loading order doesn't matter for event routing settings Initialize EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS and EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS if they aren't already, and append our events to them. ERB will do the same to preserve our settings if they're loaded first. --- completion_aggregator/settings/common.py | 6 ++++++ test_settings.py | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/completion_aggregator/settings/common.py b/completion_aggregator/settings/common.py index 04e225b8..c60cd4db 100644 --- a/completion_aggregator/settings/common.py +++ b/completion_aggregator/settings/common.py @@ -46,6 +46,12 @@ def plugin_settings(settings): 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}' diff --git a/test_settings.py b/test_settings.py index 6b13eaa6..f634d756 100644 --- a/test_settings.py +++ b/test_settings.py @@ -87,8 +87,6 @@ def root(*args): # Enables event tracking in the tests, see https://github.com/openedx/event-tracking EVENT_TRACKING_ENABLED = True EVENT_TRACKING_BACKENDS = {} -EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS = [] -EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS = [] # 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. From 812188c27034c3d4b61f1f33627f1008e2faf84a Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jun 2024 20:53:39 +0930 Subject: [PATCH 21/23] chore: bumps edx-event-routing-backends to 9.3.0 --- requirements/base.txt | 2 +- requirements/constraints.txt | 4 ---- requirements/dev.txt | 2 +- requirements/doc.txt | 2 +- requirements/quality.txt | 2 +- requirements/test.txt | 2 +- 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 55ba8910..a77c7ca6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -130,7 +130,7 @@ edx-django-utils==5.10.1 # openedx-events edx-drf-extensions==10.2.0 # via edx-completion -edx-event-routing-backends @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx +edx-event-routing-backends==9.3.0 # via # -c requirements/constraints.txt # -r requirements/base.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 5b938334..2c112072 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -16,7 +16,3 @@ # https://github.com/openedx/completion/blob/v4.2.0/completion/__init__.py#L7 pytest<6.0.0 pluggy<1.0.0 - - -# TODO Temporary constraint to fix issues with using event-routing-backends -git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx#egg=edx_event_routing_backends diff --git a/requirements/dev.txt b/requirements/dev.txt index 7fcd4df0..a2429c01 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -216,7 +216,7 @@ edx-drf-extensions==10.2.0 # via # -r requirements/quality.txt # edx-completion -edx-event-routing-backends @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx +edx-event-routing-backends==9.3.0 # via # -c requirements/constraints.txt # -r requirements/quality.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index e7d494f9..ffa1e216 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -236,7 +236,7 @@ edx-drf-extensions==10.2.0 # -r requirements/base.txt # -r requirements/test.txt # edx-completion -edx-event-routing-backends @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx +edx-event-routing-backends==9.3.0 # via # -r requirements/base.txt # -r requirements/test.txt diff --git a/requirements/quality.txt b/requirements/quality.txt index 92723dc9..69f2f828 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -201,7 +201,7 @@ edx-drf-extensions==10.2.0 # via # -r requirements/test.txt # edx-completion -edx-event-routing-backends @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx +edx-event-routing-backends==9.3.0 # via # -c requirements/constraints.txt # -r requirements/test.txt diff --git a/requirements/test.txt b/requirements/test.txt index 62148a60..2dd35c8f 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -188,7 +188,7 @@ edx-drf-extensions==10.2.0 # via # -r requirements/base.txt # edx-completion -edx-event-routing-backends @ git+https://github.com/open-craft/event-routing-backends.git@jill/fix-no-openedx +edx-event-routing-backends==9.3.0 # via # -c requirements/constraints.txt # -r requirements/base.txt From a4858e58b6a086962a8e251a7519f86c008f06c2 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jun 2024 21:04:59 +0930 Subject: [PATCH 22/23] chore: bumps version to 4.2.0 --- CHANGELOG.rst | 6 ++++++ completion_aggregator/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) 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/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' From 38507892a655a8643e577e0e287f3882340ffa52 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Thu, 20 Jun 2024 21:05:14 +0930 Subject: [PATCH 23/23] docs: adds note about xAPI to README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.