Skip to content

Commit

Permalink
feat: add completion transformers
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-canon committed Dec 13, 2024
1 parent 428cded commit 8984c42
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 33 deletions.
142 changes: 134 additions & 8 deletions completion_aggregator/xapi.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,104 @@
"""
Transformers for completion aggregation.
"""

from event_routing_backends.processors.openedx_filters.decorators import openedx_filter
from event_routing_backends.processors.xapi import constants
from event_routing_backends.processors.xapi.registry import XApiTransformersRegistry
from event_routing_backends.processors.xapi.transformer import XApiTransformer
from tincan import Activity, ActivityDefinition, Extensions, LanguageMap, Result, Verb
from tincan import Activity, ActivityDefinition, LanguageMap, Result, Verb

from django.utils.functional import cached_property

XAPI_ACTIVITY_LESSON = "http://adlnet.gov/expapi/activities/lesson"


class BaseCompletionTransformer(XApiTransformer):
"""
Base transformer for completion events.
"""

_verb = Verb(
id=constants.XAPI_VERB_COMPLETED,
display=LanguageMap({constants.EN: constants.COMPLETED}),
)
object_type = None
object_id = None

@openedx_filter(
filter_type="completion_aggregator.xapi.completion.get_object",
)
def get_object(self):
"""
Get object for xAPI transformed event.
Returns
-------
`Activity`
"""
if not self.object_type or not self.object_id:
raise NotImplementedError()

return Activity(
id=self.object_id,
definition=ActivityDefinition(
type=self.object_type,
),
)


@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.chapter")
@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.sequential")
class ModuleCompletionTransformer(BaseCompletionTransformer):
"""
Transformer for events generated when a user completes a section or subsection.
"""

object_type = constants.XAPI_ACTIVITY_MODULE

@cached_property
def object_id(self):
"""Returns the object identifier for the module completion transformer."""
return super().get_object_iri("xblock", self.get_data("data.block_id", required=True))


@XApiTransformersRegistry.register("openedx.completion_aggregator.completion.vertical")
class LessonCompletionTransformer(ModuleCompletionTransformer):
"""
Transformer for events generated when a user completes an unit.
"""

object_type = getattr(constants, "XAPI_ACTIVITY_LESSON", XAPI_ACTIVITY_LESSON)


@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

@cached_property
def object_id(self):
"""Returns the object identifier for the course completion transformer."""
return super().get_object_iri("courses", self.get_data("data.course_id", required=True))

def get_context_activities(self):
"""
Retunrs context activities property.
The XApiTransformer class implements this method and returns in the parent key
an activity that contains the course metadata however this is not necessary in
cases where a transformer uses the course metadata as object since the data is
redundant and a course cannot be its own parent, therefore this must return None.
Returns
-------
None
"""
return None


class BaseProgressTransformer(XApiTransformer):
Expand All @@ -32,7 +124,7 @@ def get_object(self) -> Activity:
raise NotImplementedError() # pragma: no cover

return Activity(
id=self.get_object_iri("xblock", self.get_data("data.block_id")),
id=self.object_id,
definition=ActivityDefinition(
type=self.object_type,
),
Expand All @@ -45,22 +137,35 @@ def get_result(self) -> Result:
progress = self.get_data("data.percent") or 0
return Result(
completion=progress == 1.0,
extensions=Extensions({
constants.XAPI_ACTIVITY_PROGRESS: (progress * 100),
}),
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.
Transformer for event generated when a user makes progress in a section or subsection.
"""

object_type = constants.XAPI_ACTIVITY_MODULE

@cached_property
def object_id(self):
"""Returns the object identifier for the module progress transformer."""
return super().get_object_iri("xblock", self.get_data("data.block_id"))


@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.vertical")
class LessonProgressTransformer(ModuleProgressTransformer):
"""
Transformer for event generated when a user makes progress in an unit.
"""

object_type = getattr(constants, "XAPI_ACTIVITY_LESSON", XAPI_ACTIVITY_LESSON)


@XApiTransformersRegistry.register("openedx.completion_aggregator.progress.course")
class CourseProgressTransformer(BaseProgressTransformer):
Expand All @@ -69,3 +174,24 @@ class CourseProgressTransformer(BaseProgressTransformer):
"""

object_type = constants.XAPI_ACTIVITY_COURSE

@cached_property
def object_id(self):
"""Returns the object identifier for the course progress transformer."""
return super().get_object_iri("courses", self.get_data("data.course_id"))

def get_context_activities(self):
"""
Retunrs context activities property.
The XApiTransformer class implements this method and returns in the parent key
an activity that contains the course metadata however this is not necessary in
cases where a transformer uses the course metadata as object since the data is
redundant and a course cannot be its own parent, therefore this must return None.
Returns
-------
None
"""
return None
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
},
"result":{
"completion":false,
"extensions": {
"https://w3id.org/xapi/cmi5/result/extensions/progress":50
"score": {
"scaled": 0.5
}
},
"timestamp":"2023-12-05T21:34:52.909063+00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"id":"146d5372-1d64-54b1-8c60-b4acaad3c976",
"object":{
"id":"http://localhost:18000/xblock/block-v1:edX+DemoX+Demo_Course+type@course+block@course",
"id":"http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course",
"definition":{
"type":"http://adlnet.gov/expapi/activities/course"
},
Expand All @@ -22,29 +22,15 @@
},
"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":"[email protected]",
"https://w3id.org/xapi/openedx/extensions/session-id":"056aca2a1c6b76742b283e73d3424453"
}
},
"result":{
"completion":false,
"extensions": {
"https://w3id.org/xapi/cmi5/result/extensions/progress":80
"score": {
"scaled": 0.8
}
},
"timestamp":"2023-12-05T21:34:52.909063+00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
},
"result":{
"completion":false,
"extensions": {
"https://w3id.org/xapi/cmi5/result/extensions/progress":60
"score": {
"scaled": 0.6
}
},
"timestamp":"2023-12-05T21:34:52.909063+00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
},
"result":{
"completion":true,
"extensions": {
"https://w3id.org/xapi/cmi5/result/extensions/progress":100
"scaled": {
"score": 0.9
}
},
"timestamp":"2023-12-05T21:34:52.909063+00:00"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"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,
"percent": 0.9,
"earned": 9,
"possible": 10
},
"context": {
Expand Down

0 comments on commit 8984c42

Please sign in to comment.