Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds xApiTransforms for completion aggregator events #205

Merged
merged 28 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ead8f61
feat: add tracking logs
andrey-canon Dec 4, 2023
a82984e
feat: addressing pr comments
andrey-canon Dec 5, 2023
4c791f9
Merge branch 'master' into jill/nelc/and/add_tracking_logs
pomegranited Jun 3, 2024
d71cffe
chore: bump edx-completion
pomegranited Jun 3, 2024
407a1d1
fix: use "openedx" as the event prefix instead of "edx"
pomegranited Jun 3, 2024
4a229fd
temp: adds edx-event-routing-backends WIP requirement
pomegranited Jun 12, 2024
7652429
feat: adds xApiTransforms for completion aggregator events
pomegranited Jun 12, 2024
bde2bdf
chore: bumps version to 4.0.4
pomegranited Jun 13, 2024
abfdb2a
test: add call to plugin_settings to fix coverage
pomegranited Jun 13, 2024
381dbd7
test: make COMPLETION_AGGREGATOR_ASYNC_AGGREGATION consistent between
pomegranited Jun 13, 2024
bb2f8e6
Merge branch 'jill/nelc/and/add_tracking_logs' into jill/nelc/and/add…
pomegranited Jun 13, 2024
e82c49d
fix: call enabled_aggregator_events with a settings arg
pomegranited Jun 13, 2024
4542458
test: adds transformer tests
pomegranited Jun 13, 2024
a1ea151
revert: no need to change production settings
pomegranited Jun 13, 2024
4d7d097
fix: address PR review
pomegranited Jun 13, 2024
b862bbc
fix: CHANGELOG use minor version bump
pomegranited Jun 13, 2024
ab96203
test: adds test for plugin settings
pomegranited Jun 13, 2024
fd1cc92
Merge branch 'jill/nelc/and/add_tracking_logs' into jill/nelc/and/add…
pomegranited Jun 13, 2024
e694f16
refactor: moves the xAPI transforms to their own module
pomegranited Jun 14, 2024
1204f91
fix: use Result.extension to send progress %
pomegranited Jun 14, 2024
29f6b89
refactor: simplify emitted events
pomegranited Jun 14, 2024
13ea455
Merge branch 'jill/nelc/and/add_tracking_logs' into jill/nelc/and/add…
pomegranited Jun 14, 2024
a142ce2
refactor: only need to transform progress events
pomegranited Jun 14, 2024
f1e6511
Merge branch 'master' into jill/nelc/and/add_completion_events
pomegranited Jun 20, 2024
3ff8331
fix: ensure plugin loading order doesn't matter for event routing set…
pomegranited Jun 20, 2024
812188c
chore: bumps edx-event-routing-backends to 9.3.0
pomegranited Jun 20, 2024
a4858e5
chore: bumps version to 4.2.0
pomegranited Jun 20, 2024
3850789
docs: adds note about xAPI to README
pomegranited Jun 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Change Log
Unreleased
~~~~~~~~~~

[4.0.4] - 2024-06-13
~~~~~~~~~~~~~~~~~~~~

* Emit tracking log events for `openedx.completion_aggregator.progress.*` and
`openedx.completion_aggregator.completion.*` for the various block/course types

[4.0.3] - 2023-10-24
~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion completion_aggregator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

from __future__ import absolute_import, unicode_literals

__version__ = '4.0.3'
__version__ = '4.0.4'
6 changes: 5 additions & 1 deletion completion_aggregator/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ def ready(self):
"""
Load signal handlers when the app is ready.
"""
# pylint: disable=import-outside-toplevel
from . import signals
signals.register()
from .tasks import aggregation_tasks, handler_tasks # pylint: disable=unused-import

# pylint: disable=unused-import
from . import transformers
from .tasks import aggregation_tasks, handler_tasks
38 changes: 38 additions & 0 deletions completion_aggregator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import absolute_import, division, print_function, unicode_literals

from eventtracking import tracker
from opaque_keys.edx.django.models import CourseKeyField, UsageKeyField
from opaque_keys.edx.keys import CourseKey, UsageKey

Expand Down Expand Up @@ -171,8 +172,44 @@ def submit_completion(self, user, course_key, block_key, aggregation_name, earne
'last_modified': last_modified,
},
)
self.emit_completion_aggregator_logs([obj])

return obj, is_new

@staticmethod
def emit_completion_aggregator_logs(updated_aggregators):
"""
Emit a tracking log for each element of the list parameter.

Parameters
----------
updated_aggregators: List of Aggregator intances

"""
for aggregator in updated_aggregators:
event = "progress" if aggregator.percent < 1 else "completion"
event_type = aggregator.aggregation_name

if event_type not in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES.get(event, {}):
continue

event_name = f"openedx.completion_aggregator.{event}.{event_type}"

tracker.emit(
event_name,
{
"user_id": aggregator.user_id,
"course_id": str(aggregator.course_key),
"block_id": str(aggregator.block_key),
"modified": aggregator.modified,
"created": aggregator.created,
"earned": aggregator.earned,
"possible": aggregator.possible,
"percent": aggregator.percent,
"type": event_type,
}
)

def bulk_create_or_update(self, updated_aggregators):
"""
Update the collection of aggregator object using mysql insert on duplicate update query.
Expand All @@ -194,6 +231,7 @@ def bulk_create_or_update(self, updated_aggregators):
else:
aggregation_data = [obj.get_values() for obj in updated_aggregators]
cur.executemany(INSERT_OR_UPDATE_AGGREGATOR_QUERY, aggregation_data)
self.emit_completion_aggregator_logs(updated_aggregators)


class Aggregator(TimeStampedModel):
Expand Down
5 changes: 5 additions & 0 deletions completion_aggregator/settings/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

from __future__ import absolute_import, division, print_function, unicode_literals

from event_routing_backends.settings import production as erb_settings


def plugin_settings(settings):
"""
Modify the provided settings object with settings specific to this plugin.
"""
# Load Event Routing Backend production settings first.
erb_settings.plugin_settings(settings)

settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = set(settings.ENV_TOKENS.get(
'COMPLETION_AGGREGATOR_BLOCK_TYPES',
settings.COMPLETION_AGGREGATOR_BLOCK_TYPES,
Expand Down
33 changes: 33 additions & 0 deletions completion_aggregator/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,37 @@

from __future__ import absolute_import, division, print_function, unicode_literals

from event_routing_backends.utils.settings import event_tracking_backends_config


def plugin_settings(settings):
"""
Modify the provided settings object with settings specific to this plugin.
"""
# Emit feature allows to publish two kind of events progress and completion
# This setting controls which type of event will be published to change the default behavior
# the block type should be removed or added from the progress or completion list.
settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES = {
"progress": {
"course",
"chapter",
"sequential",
"vertical",
},
"completion": {
"course",
"chapter",
"sequential",
"vertical",
}
}
settings.COMPLETION_AGGREGATOR_BLOCK_TYPES = {
'course',
'chapter',
'sequential',
'vertical',
}

settings.COMPLETION_AGGREGATOR_ASYNC_AGGREGATION = False

# Names of the batch operations locks
Expand All @@ -35,3 +55,16 @@ def plugin_settings(settings):
# 1. All courses should be reaggregated for the changes to take effect.
# 2. It's not possible to revert this change by reaggregation without manually removing existing Aggregators.
settings.COMPLETION_AGGREGATOR_AGGREGATE_UNRELEASED_BLOCKS = False

# Whitelist the aggregator events for use with event routing backends xAPI backend.
enabled_aggregator_events = [
f'openedx.completion_aggregator.{event_type}.{block_type}'

for event_type in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES
for block_type in settings.ALLOWED_COMPLETION_AGGREGATOR_EVENT_TYPES[event_type]
]
settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS += enabled_aggregator_events
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
settings.EVENT_TRACKING_BACKENDS.update(event_tracking_backends_config(
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
settings.EVENT_TRACKING_BACKENDS_ALLOWED_XAPI_EVENTS,
settings.EVENT_TRACKING_BACKENDS_ALLOWED_CALIPER_EVENTS,
))
121 changes: 121 additions & 0 deletions completion_aggregator/transformers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
except ImportError:
BlockStructureTransformer = object

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, LanguageMap, Result, Verb
from xblock.completable import XBlockCompletionMode


Expand Down Expand Up @@ -79,3 +84,119 @@ def transform(self, usage_info, block_structure): # pylint: disable=unused-argu
if completion_mode != XBlockCompletionMode.EXCLUDED:
aggregators = self.calculate_aggregators(block_structure, block_key)
block_structure.set_transformer_block_field(block_key, self, self.AGGREGATORS, aggregators)


class BaseAggregatorXApiTransformer(XApiTransformer):
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
"""
Base transformer for all completion aggregator events.
"""

object_type = None

def get_object(self) -> Activity:
"""
Get object for xAPI transformed event.
"""
if not self.object_type:
raise NotImplementedError()

return Activity(
id=self.get_object_iri("xblock", self.get_data("data.block_id")),
definition=ActivityDefinition(
type=self.object_type,
),
)


class BaseProgressTransformer(BaseAggregatorXApiTransformer):
"""
Base transformer for completion aggregator progress events.
"""

_verb = Verb(
id=constants.XAPI_VERB_PROGRESSED,
display=LanguageMap({constants.EN: constants.PROGRESSED}),
)
object_type = None
additional_fields = ('result', )

@openedx_filter(
filter_type="completion_aggregator.xapi.base_progress.get_object",
)
def get_object(self) -> Activity:
"""
Get object for xAPI transformed event.
"""
return super().get_object()

def get_result(self) -> Result:
"""
Get result for xAPI transformed event.
"""
return Result(
completion=self.get_data("data.percent") == 1.0,
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
score={
"scaled": self.get_data("data.percent") or 0
Agrendalath marked this conversation as resolved.
Show resolved Hide resolved
}
)


@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.
"""

object_type = constants.XAPI_ACTIVITY_MODULE


@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


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

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

@openedx_filter(
filter_type="completion_aggregator.xapi.base_completion.get_object",
)
def get_object(self) -> Activity:
"""
Get object for xAPI transformed event.
"""
return super().get_object()


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

object_type = constants.XAPI_ACTIVITY_MODULE


@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
2 changes: 2 additions & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ django-model-utils # Provides TimeStampedModel abstract base class
edx-opaque-keys # Provides CourseKey and UsageKey
edx-completion
edx-toggles
event-tracking
six
XBlock[django]
edx-event-routing-backends # Provides xAPI transforms for aggregator events
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading