Skip to content

Commit

Permalink
feat: add xapi transformer for exam attempts events
Browse files Browse the repository at this point in the history
fix: add attempt type for exam attempt events

fix: add context data to event attempt events

test: add fixtures for special exams

refactor: use exam object for event attempts

fix: add missing xapi concepts for exam events

fix: use scorm pattern for exam events

test: add fixtures for special exams

fix: remove proctored and timed extensions

fix: add duration extension

fix: do not override get context extensions

feat: include practice and proctored exam events

fix: use attempt-id for attempt events
  • Loading branch information
Ian2012 committed Sep 13, 2023
1 parent 41c960b commit 69f4748
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 2 deletions.
8 changes: 8 additions & 0 deletions docs/event-mapping/Supported_events.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 11 additions & 2 deletions event_routing_backends/processors/tests/transformers_test_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 10 additions & 0 deletions event_routing_backends/processors/xapi/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_FORMAT = 'https://w3id.org/xapi/openedx/activity/{}-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'
Expand All @@ -58,6 +59,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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
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")
exam_type = self.get_data("name").split(".")[2]

return Activity(
id=object_id,
definition=ActivityDefinition(
type=constants.XAPI_ACTIVITY_ASSESSMENT_FORMAT.format(exam_type),
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


@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.created")
@XApiTransformersRegistry.register("edx.special_exam.practice.attempt.created")
@XApiTransformersRegistry.register("edx.special_exam.proctored.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}),
)


@XApiTransformersRegistry.register("edx.special_exam.timed.attempt.submitted")
@XApiTransformersRegistry.register("edx.special_exam.practice.attempt.created")
@XApiTransformersRegistry.register("edx.special_exam.proctored.attempt.created")
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}),
)
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80"
}
}
}
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"https://w3id.org/xapi/openedx/extensions/session-id": "1c7862f091c5d7232ad3d7cf558f6e80"
}
}
}
2 changes: 2 additions & 0 deletions event_routing_backends/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 69f4748

Please sign in to comment.