From b1869a09d89b450bb94b46547f02fbb7494d5ee2 Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Tue, 19 Sep 2023 08:05:52 -0500 Subject: [PATCH] feat: add xapi transformer for exam attempts events (#349) --- .gitignore | 2 + CHANGELOG.rst | 8 + docs/event-mapping/Supported_events.rst | 14 ++ event_routing_backends/__init__.py | 2 +- ...special_exam.practice.attempt.created.json | 38 +++++ ...ecial_exam.practice.attempt.submitted.json | 38 +++++ ...pecial_exam.proctored.attempt.created.json | 38 +++++ ...cial_exam.proctored.attempt.submitted.json | 38 +++++ ...dx.special_exam.timed.attempt.created.json | 38 +++++ ....special_exam.timed.attempt.submitted.json | 38 +++++ .../tests/transformers_test_mixin.py | 11 +- .../processors/xapi/constants.py | 12 ++ .../xapi/event_transformers/__init__.py | 8 + .../xapi/event_transformers/exam_events.py | 160 ++++++++++++++++++ ...special_exam.practice.attempt.created.json | 52 ++++++ ...ecial_exam.practice.attempt.submitted.json | 55 ++++++ ...pecial_exam.proctored.attempt.created.json | 52 ++++++ ...cial_exam.proctored.attempt.submitted.json | 55 ++++++ ...dx.special_exam.timed.attempt.created.json | 52 ++++++ ....special_exam.timed.attempt.submitted.json | 55 ++++++ event_routing_backends/settings/common.py | 6 + test_output/.keep | 0 22 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.created.json create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.submitted.json create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.created.json create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.submitted.json create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.created.json create mode 100644 event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.submitted.json create mode 100644 event_routing_backends/processors/xapi/event_transformers/exam_events.py create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.created.json create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.submitted.json create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.created.json create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.submitted.json create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.created.json create mode 100644 event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.json create mode 100644 test_output/.keep diff --git a/.gitignore b/.gitignore index 27a35047..2b53fdcb 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ requirements/private.in requirements/private.txt .idea + +test_output/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6853e83..981476d5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,14 @@ Change Log Unreleased ~~~~~~~~~~ +[6.1.0] + +* Add support for exam attempts events + +[6.0.0] + +* Do not send events for unknown users + [5.5.6] * upgrading deprecated `djfernet` with `django-fernet-fields-v2` diff --git a/docs/event-mapping/Supported_events.rst b/docs/event-mapping/Supported_events.rst index 92b980c6..8aa72245 100644 --- a/docs/event-mapping/Supported_events.rst +++ b/docs/event-mapping/Supported_events.rst @@ -72,6 +72,14 @@ Forum events * `edx.forum.comment.reported`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.forum.comment.reported.json>`__ | xAPI `map <./xAPI_mapping.rst#edx.forum.comment.reported>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.forum.comment.reported.json>`__ * `edx.forum.comment.unreported`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.forum.comment.unreported.json>`__ | xAPI `map <./xAPI_mapping.rst#edx.forum.comment.unreported>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.forum.comment.unreported.json>`__ +Exam events +------------------ + +* `edx.special_exam.timed.attempt.created`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.started.json>`__ | xAPI `map <./xAPI_mapping.rst#edx-special-exam-timed-attempt-started>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.started.json>`__ +* `edx.special_exam.timed.attempt.submitted`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.submitted.json>`__ | xAPI `map <./xAPI_mapping.rst#edx-special-exam-timed-attempt-submitted>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.json>`__ +* `edx.special_exam.proctored.attempt.created`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.started.json>`__ | xAPI `map <./xAPI_mapping.rst#edx-special-exam-proctored-attempt-started>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.started.json>`__ +* `edx.special_exam.proctored.attempt.submitted`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.submitted.json>`__ | xAPI `map <./xAPI_mapping.rst#edx-special-exam-proctored-attempt-submitted>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.submitted.json>`__ +* `edx.special_exam.practice.attempt.submitted`_ | edX `sample <../../event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.submitted.json>`__ | xAPI `map <./xAPI_mapping.rst#edx-special-exam-practice-attempt-submitted>`__ , `sample <../../event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.submitted.json>`__ .. _edx.course.enrollment.activated: http://edx.readthedocs.io/projects/devdata/en/latest/internal_data_formats/tracking_logs/student_event_types.html#edx-course-enrollment-activated-and-edx-course-enrollment-deactivated .. _edx.course.enrollment.deactivated: http://edx.readthedocs.io/projects/devdata/en/latest/internal_data_formats/tracking_logs/student_event_types.html#edx-course-enrollment-activated-and-edx-course-enrollment-deactivated @@ -115,3 +123,9 @@ Forum events .. _edx.forum.comment.deleted: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-forum-comment-deleted .. _edx.forum.comment.reported: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-forum-comment-reported .. _edx.forum.comment.unreported: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-forum-comment-unreported +.. _edx.special_exam.timed.attempt.created: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-created-edx-special-exam-practice-attempt-created-and-edx-special-exam-timed-attempt-created +.. _edx.special_exam.timed.attempt.submitted: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-submitted-edx-special-exam-practice-attempt-submitted-and-edx-special-exam-timed-attempt-submitted +.. _edx.special_exam.proctored.attempt.created: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-created-edx-special-exam-practice-attempt-created-and-edx-special-exam-timed-attempt-created +.. _edx.special_exam.proctored.attempt.submitted: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-submitted-edx-special-exam-practice-attempt-submitted-and-edx-special-exam-timed-attempt-submitted +.. _edx.special_exam.practice.attempt.created: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-created-edx-special-exam-practice-attempt-created-and-edx-special-exam-timed-attempt-created +.. _edx.special_exam.practice.attempt.submitted: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-submitted-edx-special-exam-practice-attempt-submitted-and-edx-special-exam-timed-attempt-submitted diff --git a/event_routing_backends/__init__.py b/event_routing_backends/__init__.py index 38bca024..65bf3aef 100644 --- a/event_routing_backends/__init__.py +++ b/event_routing_backends/__init__.py @@ -2,4 +2,4 @@ Various backends for receiving edX LMS events.. """ -__version__ = '6.0.0' +__version__ = '6.1.0' diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.created.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.created.json new file mode 100644 index 00000000..6711e9fd --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.created.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.practice.attempt.created", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": false, + "exam_is_practice_exam": true, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": null, + "attempt_completed_at": null, + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": null, + "attempt_status": "created", + "attempt_event_elapsed_time_secs": null + }, + "time": "2023-09-08T15:58:04.833393+00:00", + "event_type": "edx.special_exam.timed.attempt.created", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.submitted.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.submitted.json new file mode 100644 index 00000000..6a23e274 --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.practice.attempt.submitted.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.practice.attempt.submitted", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt/1", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": false, + "exam_is_practice_exam": true, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": "2023-09-08T15:58:04.838598+00:00", + "attempt_completed_at": "2023-09-08T16:20:53.577698+00:00", + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": 60, + "attempt_status": "submitted", + "attempt_event_elapsed_time_secs": 1368.7657 + }, + "time": "2023-09-08T16:20:53.604375+00:00", + "event_type": "edx.special_exam.timed.attempt.submitted", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.created.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.created.json new file mode 100644 index 00000000..8fef4675 --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.created.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.proctored.attempt.created", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": true, + "exam_is_practice_exam": false, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": null, + "attempt_completed_at": null, + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": null, + "attempt_status": "created", + "attempt_event_elapsed_time_secs": null + }, + "time": "2023-09-08T15:58:04.833393+00:00", + "event_type": "edx.special_exam.timed.attempt.created", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.submitted.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.submitted.json new file mode 100644 index 00000000..fc6f401f --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.proctored.attempt.submitted.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.proctored.attempt.submitted", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt/1", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": true, + "exam_is_practice_exam": false, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": "2023-09-08T15:58:04.838598+00:00", + "attempt_completed_at": "2023-09-08T16:20:53.577698+00:00", + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": 60, + "attempt_status": "submitted", + "attempt_event_elapsed_time_secs": 1368.7657 + }, + "time": "2023-09-08T16:20:53.604375+00:00", + "event_type": "edx.special_exam.timed.attempt.submitted", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.created.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.created.json new file mode 100644 index 00000000..d2f15df6 --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.created.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.timed.attempt.created", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": false, + "exam_is_practice_exam": false, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": null, + "attempt_completed_at": null, + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": null, + "attempt_status": "created", + "attempt_event_elapsed_time_secs": null + }, + "time": "2023-09-08T15:58:04.833393+00:00", + "event_type": "edx.special_exam.timed.attempt.created", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.submitted.json b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.submitted.json new file mode 100644 index 00000000..4137bd64 --- /dev/null +++ b/event_routing_backends/processors/tests/fixtures/current/edx.special_exam.timed.attempt.submitted.json @@ -0,0 +1,38 @@ +{ + "name": "edx.special_exam.timed.attempt.submitted", + "context": { + "user_id": 3, + "path": "/api/edx_proctoring/v1/proctored_exam/attempt/1", + "course_id": "course-v1:edX+DemoX+Demo_Course", + "org_id": "edX", + "enterprise_uuid": "" + }, + "username": "student", + "session": "1c7862f091c5d7232ad3d7cf558f6e80", + "ip": "172.18.0.1", + "agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", + "host": "localhost:18000", + "referer": "http://localhost:18000", + "accept_language": "en-US,en;q=0.9", + "event": { + "exam_id": 1, + "exam_content_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "exam_name": "Subsection", + "exam_default_time_limit_mins": 60, + "exam_is_proctored": false, + "exam_is_practice_exam": false, + "exam_is_active": true, + "attempt_id": 1, + "attempt_user_id": 3, + "attempt_started_at": "2023-09-08T15:58:04.838598+00:00", + "attempt_completed_at": "2023-09-08T16:20:53.577698+00:00", + "attempt_code": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "attempt_allowed_time_limit_mins": 60, + "attempt_status": "submitted", + "attempt_event_elapsed_time_secs": 1368.7657 + }, + "time": "2023-09-08T16:20:53.604375+00:00", + "event_type": "edx.special_exam.timed.attempt.submitted", + "event_source": "server", + "page": null +} diff --git a/event_routing_backends/processors/tests/transformers_test_mixin.py b/event_routing_backends/processors/tests/transformers_test_mixin.py index 064232f2..083c1c62 100644 --- a/event_routing_backends/processors/tests/transformers_test_mixin.py +++ b/event_routing_backends/processors/tests/transformers_test_mixin.py @@ -114,4 +114,13 @@ def test_event_transformer(self, event_filename, mocked_uuid): self.registry.get_transformer(original_event).transform() else: actual_transformed_event = self.registry.get_transformer(original_event).transform() - self.compare_events(actual_transformed_event, expected_event) + try: + self.compare_events(actual_transformed_event, expected_event) + except Exception as e: # pragma: no cover + with open(f"test_output/generated.{event_filename}.json", "w") as actual_transformed_event_file: + actual_transformed_event_file.write(actual_transformed_event.to_json()) + + with open(f"test_output/expected.{event_filename}.json", "w") as expected_event_file: + json.dump(expected_event, expected_event_file, indent=4) + + raise e diff --git a/event_routing_backends/processors/xapi/constants.py b/event_routing_backends/processors/xapi/constants.py index 2d8308c4..d6d54b23 100644 --- a/event_routing_backends/processors/xapi/constants.py +++ b/event_routing_backends/processors/xapi/constants.py @@ -50,6 +50,9 @@ XAPI_ACTIVITY_ATTEMPT = 'http://id.tincanapi.com/extension/attempt-id' XAPI_ACTIVITY_GRADE_CLASSIFICATION = 'http://www.tincanapi.co.uk/activitytypes/grade_classification' XAPI_ACTIVITY_GRADE = 'http://www.tincanapi.co.uk/extensions/result/classification' +XAPI_ACTIVITY_TIMED_ASSESSMENT = 'https://w3id.org/xapi/openedx/activity/timed-assessment' +XAPI_ACTIVITY_PRACTICE_ASSESSMENT = 'https://w3id.org/xapi/openedx/activity/practice-assessment' +XAPI_ACTIVITY_PROCTORED_ASSESSMENT = 'https://w3id.org/xapi/openedx/activity/proctored-assessment' # xAPI context XAPI_CONTEXT_VIDEO_LENGTH = 'https://w3id.org/xapi/video/extensions/length' XAPI_CONTEXT_VIDEO_CC_LANGUAGE = 'https://w3id.org/xapi/video/extensions/cc-subtitle-lang' @@ -58,6 +61,15 @@ XAPI_CONTEXT_COMPLETION_THRESHOLD = 'https://w3id.org/xapi/video/extensions/completion-threshold' XAPI_CONTEXT_SESSION_ID = 'https://w3id.org/xapi/openedx/extensions/session-id' +XAPI_ACTIVITY_TIME_LIMIT = 'https://w3id.org/xapi/acrossx/extensions/time-limit' + +XAPI_ACTIVITY_EXAM_ATTEMPT = 'http://adlnet.gov/expapi/activities/attempt' + +XAPI_CONTEXT_ATTEMPT_STARTED = 'https://w3id.org/xapi/openedx/extension/attempt-started' +XAPI_CONTEXT_ATTEMPT_COMPLETED = 'https://w3id.org/xapi/openedx/extension/attempt-completed' +XAPI_CONTEXT_DURATION = 'http://id.tincanapi.com/extension/duration' +XAPI_CONTEXT_CODE = 'https://w3id.org/xapi/openedx/extension/code' + # xAPI result XAPI_RESULT_VIDEO_TIME = 'https://w3id.org/xapi/video/extensions/time' XAPI_RESULT_VIDEO_TIME_FROM = 'https://w3id.org/xapi/video/extensions/time-from' diff --git a/event_routing_backends/processors/xapi/event_transformers/__init__.py b/event_routing_backends/processors/xapi/event_transformers/__init__.py index d5c60147..5a285b3b 100644 --- a/event_routing_backends/processors/xapi/event_transformers/__init__.py +++ b/event_routing_backends/processors/xapi/event_transformers/__init__.py @@ -6,6 +6,14 @@ EnrollmentActivatedTransformer, EnrollmentDeactivatedTransformer, ) +from event_routing_backends.processors.xapi.event_transformers.exam_events import ( + PracticeExamCreatedTransformer, + PracticeExamSubmittedTransformer, + ProctoredExamCreatedTransformer, + ProctoredExamSubmittedTransformer, + TimedExamCreatedTransformer, + TimedExamSubmittedTransformer, +) from event_routing_backends.processors.xapi.event_transformers.forum_events import ( ThreadCreatedTransformer, ThreadDeletedTransformer, diff --git a/event_routing_backends/processors/xapi/event_transformers/exam_events.py b/event_routing_backends/processors/xapi/event_transformers/exam_events.py new file mode 100644 index 00000000..66ed0164 --- /dev/null +++ b/event_routing_backends/processors/xapi/event_transformers/exam_events.py @@ -0,0 +1,160 @@ +""" +Transformers for enrollment related events. +""" + +from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Verb + +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 + + +class BaseExamTransformer(XApiTransformer): + """ + Base transformer for exam events. + """ + + exam_type_activity = None + + def get_object(self): + """ + Get object for xAPI transformed event. + + Returns: + `Activity` + """ + object_id = self.get_data("event.exam_content_id") + + return Activity( + id=object_id, + definition=ActivityDefinition( + type=self.exam_type_activity, + name=LanguageMap(**({constants.EN: self.get_data("event.exam_name")})), + extensions=Extensions( + { + constants.XAPI_ACTIVITY_TIME_LIMIT: self.get_data( + "event.exam_default_time_limit_mins" + ) + } + ), + ), + ) + + def get_context_activities(self): + context_activities = super().get_context_activities() + + context_activities.grouping = [ + Activity( + id=self.get_data("event.attempt_code"), + definition=ActivityDefinition( + type=constants.XAPI_ACTIVITY_EXAM_ATTEMPT, + name=LanguageMap({constants.EN: self.get_data("event.exam_name")}), + extensions=Extensions( + { + constants.XAPI_CONTEXT_ATTEMPT_STARTED: self.get_data( + "event.attempt_started_at" + ), + constants.XAPI_CONTEXT_ATTEMPT_COMPLETED: self.get_data( + "event.attempt_completed_at" + ), + constants.XAPI_CONTEXT_DURATION: self.get_data( + "event.attempt_event_elapsed_time_secs" + ), + constants.XAPI_ACTIVITY_ATTEMPT: self.get_data( + "event.attempt_id" + ), + } + ), + ), + ), + ] + + return context_activities + + +class BaseTimedExamTransformer(BaseExamTransformer): + """ + Base transformer for timed exam events. + """ + + exam_type_activity = constants.XAPI_ACTIVITY_TIMED_ASSESSMENT + + +class BasePracticeExamTransformer(BaseExamTransformer): + """ + Base transformer for practice exam events. + """ + + exam_type_activity = constants.XAPI_ACTIVITY_PRACTICE_ASSESSMENT + + +class BaseProctoredExamTransformer(BaseExamTransformer): + """ + Base transformer for proctored exam events. + """ + + exam_type_activity = constants.XAPI_ACTIVITY_PROCTORED_ASSESSMENT + + +class InitializedMixin: + """ + Base transformer for initialized exam events + """ + + verb = Verb( + id=constants.XAPI_VERB_INITIALIZED, + display=LanguageMap({constants.EN: constants.INITIALIZED}), + ) + + +class TerminatedMixin: + """ + Base transformer for terminated exam events + """ + + verb = Verb( + id=constants.XAPI_VERB_TERMINATED, + display=LanguageMap({constants.EN: constants.TERMINATED}), + ) + + +@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.created") +class TimedExamCreatedTransformer(BaseTimedExamTransformer, InitializedMixin): + """ + Transformers for event generated when learner start an exam attempt. + """ + + +@XApiTransformersRegistry.register("edx.special_exam.practice.attempt.created") +class PracticeExamCreatedTransformer(BasePracticeExamTransformer, InitializedMixin): + """ + Transformers for event generated when learner start an exam attempt. + """ + + +@XApiTransformersRegistry.register("edx.special_exam.proctored.attempt.created") +class ProctoredExamCreatedTransformer(BaseProctoredExamTransformer, InitializedMixin): + """ + Transformers for event generated when learner start an exam attempt. + """ + + +@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.submitted") +class TimedExamSubmittedTransformer(BaseTimedExamTransformer, TerminatedMixin): + """ + Transformers for event generated when learner submit an exam attempt. + """ + + +@XApiTransformersRegistry.register("edx.special_exam.practice.attempt.submitted") +class PracticeExamSubmittedTransformer(BasePracticeExamTransformer, TerminatedMixin): + """ + Transformers for event generated when learner submit an exam attempt. + """ + + +@XApiTransformersRegistry.register("edx.special_exam.proctored.attempt.submitted") +class ProctoredExamSubmittedTransformer(BaseProctoredExamTransformer, TerminatedMixin): + """ + Transformers for event generated when learner submit an exam attempt. + """ diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.created.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.created.json new file mode 100644 index 00000000..e57d2db7 --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.created.json @@ -0,0 +1,52 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/initialized", "display": { "en": "initialized" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/practice-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T15:58:04.833393+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.submitted.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.submitted.json new file mode 100644 index 00000000..dc437b4b --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.practice.attempt.submitted.json @@ -0,0 +1,55 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/terminated", "display": { "en": "terminated" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/practice-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T16:20:53.604375+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "https://w3id.org/xapi/openedx/extension/attempt-started": "2023-09-08T15:58:04.838598+00:00", + "https://w3id.org/xapi/openedx/extension/attempt-completed": "2023-09-08T16:20:53.577698+00:00", + "http://id.tincanapi.com/extension/duration": 1368.7657, + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.created.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.created.json new file mode 100644 index 00000000..795c5cd7 --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.created.json @@ -0,0 +1,52 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/initialized", "display": { "en": "initialized" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/proctored-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T15:58:04.833393+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.submitted.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.submitted.json new file mode 100644 index 00000000..fef793a3 --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.proctored.attempt.submitted.json @@ -0,0 +1,55 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/terminated", "display": { "en": "terminated" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/proctored-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T16:20:53.604375+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "https://w3id.org/xapi/openedx/extension/attempt-started": "2023-09-08T15:58:04.838598+00:00", + "https://w3id.org/xapi/openedx/extension/attempt-completed": "2023-09-08T16:20:53.577698+00:00", + "http://id.tincanapi.com/extension/duration": 1368.7657, + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.created.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.created.json new file mode 100644 index 00000000..1fc4f26d --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.created.json @@ -0,0 +1,52 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/initialized", "display": { "en": "initialized" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/timed-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T15:58:04.833393+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.json b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.json new file mode 100644 index 00000000..4c54f35b --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.json @@ -0,0 +1,55 @@ +{ + "id": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", + "version": "1.0.3", + "actor": { + "objectType": "Agent", + "account": { "name": "32e08e30-f8ae-4ce2-94a8-c2bfe38a70cb", "homePage": "http://localhost:18000" } + }, + "verb": { "id": "http://adlnet.gov/expapi/verbs/terminated", "display": { "en": "terminated" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "https://w3id.org/xapi/openedx/activity/timed-assessment", + "extensions": { + "https://w3id.org/xapi/acrossx/extensions/time-limit": 60 + } + } + }, + "timestamp": "2023-09-08T16:20:53.604375+00:00", + "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" + } + } + ], + "grouping": [ + { + "id": "438AD672-DE2C-4F0B-8876-35444E7DD746", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/attempt", + "extensions": { + "https://w3id.org/xapi/openedx/extension/attempt-started": "2023-09-08T15:58:04.838598+00:00", + "https://w3id.org/xapi/openedx/extension/attempt-completed": "2023-09-08T16:20:53.577698+00:00", + "http://id.tincanapi.com/extension/duration": 1368.7657, + "http://id.tincanapi.com/extension/attempt-id": 1 + } + } + } + ] + }, + "extensions": { + "https://w3id.org/xapi/openedx/extension/transformer-version": "event-routing-backends@1.1.1", + "https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80" + } + } +} diff --git a/event_routing_backends/settings/common.py b/event_routing_backends/settings/common.py index 02c1b671..67428431 100644 --- a/event_routing_backends/settings/common.py +++ b/event_routing_backends/settings/common.py @@ -74,6 +74,12 @@ def plugin_settings(settings): 'edx.course.enrollment.mode_changed', 'edx.grades.subsection.grade_calculated', 'edx.grades.course.grade_calculated', + 'edx.special_exam.timed.attempt.created', + 'edx.special_exam.timed.attempt.submitted', + 'edx.special_exam.practice.attempt.created', + 'edx.special_exam.practice.attempt.submitted', + 'edx.special_exam.proctored.attempt.created', + 'edx.special_exam.proctored.attempt.submitted', 'edx.forum.thread.created', 'edx.forum.thread.deleted', 'edx.forum.thread.edited', diff --git a/test_output/.keep b/test_output/.keep new file mode 100644 index 00000000..e69de29b