diff --git a/docs/event-mapping/Supported_events.rst b/docs/event-mapping/Supported_events.rst index 92b980c6..b52c2dce 100644 --- a/docs/event-mapping/Supported_events.rst +++ b/docs/event-mapping/Supported_events.rst @@ -72,6 +72,12 @@ 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.started`_ | 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.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 +121,5 @@ 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.started: https://docs.openedx.org/en/latest/developers/references/internal_data_formats/tracking_logs/student_event_types.html#edx-special-exam-proctored-attempt-started-edx-special-exam-practice-attempt-started-and-edx-special-exam-timed-attempt-started +.. _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 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..21df2bde 100644 --- a/event_routing_backends/processors/tests/transformers_test_mixin.py +++ b/event_routing_backends/processors/tests/transformers_test_mixin.py @@ -113,5 +113,14 @@ def test_event_transformer(self, event_filename, mocked_uuid): with pytest.raises(ValueError): 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: + actual_transformed_event = self.registry.get_transformer(original_event).transform() + self.compare_events(actual_transformed_event, expected_event) + except Exception as e: # pragma: no cover + with open("actual_transformed_event.json", "w") as actual_transformed_event_file: + actual_transformed_event_file.write(actual_transformed_event.to_json()) + + with open("expected_event.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..e99dad40 100644 --- a/event_routing_backends/processors/xapi/constants.py +++ b/event_routing_backends/processors/xapi/constants.py @@ -50,6 +50,7 @@ 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_ASSESSMENT = 'http://adlnet.gov/expapi/activities/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 +59,19 @@ 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_PRACTICE_EXAM = 'https://w3id.org/xapi/openedx/extension/practice-exam' +XAPI_ACTIVITY_PROCTORED_EXAM = 'https://w3id.org/xapi/openedx/extension/proctored-exam' + +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_ATTEMPT_TIME = 'https://w3id.org/xapi/openedx/extension/attempt-time' +XAPI_CONTEXT_ID = 'https://w3id.org/xapi/openedx/extension/id' +XAPI_CONTEXT_CODE = 'https://w3id.org/xapi/openedx/extension/code' + +XAPI_CONTEXT_ATTEMPT_TYPE = 'https://w3id.org/xapi/openedx/extension/attempt-type' + # 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..0b40297a 100644 --- a/event_routing_backends/processors/xapi/event_transformers/__init__.py +++ b/event_routing_backends/processors/xapi/event_transformers/__init__.py @@ -6,6 +6,10 @@ EnrollmentActivatedTransformer, EnrollmentDeactivatedTransformer, ) +from event_routing_backends.processors.xapi.event_transformers.exam_events import ( + PracticeExamStartedTransformer, + PracticeExamSubmittedTransformer, +) 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..cfae74c1 --- /dev/null +++ b/event_routing_backends/processors/xapi/event_transformers/exam_events.py @@ -0,0 +1,110 @@ +""" +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. + """ + + 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=constants.XAPI_ACTIVITY_ASSESSMENT, + name=LanguageMap(**({constants.EN: self.get_data("event.exam_name")})), + extensions=Extensions( + { + constants.XAPI_ACTIVITY_PRACTICE_EXAM: self.get_data( + "event.exam_is_practice_exam" + ), + constants.XAPI_ACTIVITY_PROCTORED_EXAM: self.get_data( + "event.exam_is_proctored" + ), + constants.XAPI_ACTIVITY_COURSE: self.get_data("event.course_id"), + } + ), + ), + ) + + 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_ATTEMPT_TIME: self.get_data( + "event.attempt_event_elapsed_time_secs" + ), + constants.XAPI_CONTEXT_ID: self.get_data("event.attempt_id"), + } + ), + ), + ), + ] + + return context_activities + + +@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.created") +class PracticeExamStartedTransformer(BaseExamTransformer): + """ + Transformers for event generated when learner start an exam attempt. + """ + + verb = Verb( + id=constants.XAPI_VERB_INITIALIZED, + display=LanguageMap({constants.EN: constants.INITIALIZED}), + ) + + def get_context_extensions(self): + return ( + super() + .get_context_extensions() + .update({constants.XAPI_CONTEXT_ATTEMPT_TYPE: "timed"}) + ) + + +@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.submitted") +class PracticeExamSubmittedTransformer(BaseExamTransformer): + """ + Transformers for event generated when learner submit an exam attempt. + """ + + verb = Verb( + id=constants.XAPI_VERB_TERMINATED, + display=LanguageMap({constants.EN: constants.TERMINATED}), + ) + + def get_context_extensions(self): + return ( + super() + .get_context_extensions() + .update({constants.XAPI_CONTEXT_ATTEMPT_TYPE: "timed"}) + ) 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..1338e393 --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.created.json @@ -0,0 +1,49 @@ +{ + "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": "http://adlnet.gov/expapi/activities/assessment", + "extensions": { + "https://w3id.org/xapi/openedx/extension/practice-exam": false, + "https://w3id.org/xapi/openedx/extension/proctored-exam": false + } + } + }, + "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": { + "https://w3id.org/xapi/openedx/extension/id": 1 + } + } + } + ] + } + } +} 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..530bb87e --- /dev/null +++ b/event_routing_backends/processors/xapi/tests/fixtures/expected/edx.special_exam.timed.attempt.submitted.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/terminated", "display": { "en": "terminated" } }, + "object": { + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@5b4bf8d7d41c4070b299abefed74155e", + "objectType": "Activity", + "definition": { + "name": { "en": "Subsection" }, + "type": "http://adlnet.gov/expapi/activities/assessment", + "extensions": { + "https://w3id.org/xapi/openedx/extension/practice-exam": false, + "https://w3id.org/xapi/openedx/extension/proctored-exam": false + } + } + }, + "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", + "https://w3id.org/xapi/openedx/extension/attempt-time": 1368.7657, + "https://w3id.org/xapi/openedx/extension/id": 1 + } + } + } + ] + } + } +} diff --git a/event_routing_backends/settings/common.py b/event_routing_backends/settings/common.py index 02c1b671..a9f6f346 100644 --- a/event_routing_backends/settings/common.py +++ b/event_routing_backends/settings/common.py @@ -74,6 +74,8 @@ 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.forum.thread.created', 'edx.forum.thread.deleted', 'edx.forum.thread.edited',