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 12, 2024
1 parent 428cded commit 7c31ddd
Showing 1 changed file with 114 additions and 9 deletions.
123 changes: 114 additions & 9 deletions completion_aggregator/xapi.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,91 @@
"""
Transformers for completion aggregation.
"""
from django.utils.functional import cached_property

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


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):
"""This property 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 = constants.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):
"""This property 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):
"""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 +111,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,27 +124,53 @@ 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):
"""This property 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 = constants.XAPI_ACTIVITY_LESSON


@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

@cached_property
def object_id(self):
"""This property 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):
"""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

0 comments on commit 7c31ddd

Please sign in to comment.