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: add tracking log for completion events #245

Merged
merged 5 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions completion/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@

from model_utils.models import TimeStampedModel

from eventtracking import tracker

from . import waffle

log = logging.getLogger(__name__)
User = auth.get_user_model()


BLOCK_COMPLETION_CHANGED_EVENT_TYPE = 'edx.completion.block_completion.changed'


def validate_percent(value):
"""
Verify that the passed value is between 0.0 and 1.0.
Expand Down Expand Up @@ -115,17 +120,21 @@ def submit_completion(self, user, block_key, completion):
block_key=block_key,
)
is_new = False

if not is_new and obj.completion != completion:
obj.completion = completion
obj.full_clean()
obj.save(update_fields={'completion', 'modified'})

obj.emit_tracking_log()
else:
# If the feature is not enabled, this method should not be called.
# Error out with a RuntimeError.
raise RuntimeError(
"BlockCompletion.objects.submit_completion should not be \
called when the feature is disabled."
)

return obj, is_new

@transaction.atomic()
Expand Down Expand Up @@ -321,3 +330,20 @@ class Meta:

def __unicode__(self):
return f'BlockCompletion: {self.user.username}, {self.context_key}, {self.block_key}: {self.completion}'

def emit_tracking_log(self):
"""
Emit a tracking log when a block completion is created or updated.
"""
tracker.emit(
BLOCK_COMPLETION_CHANGED_EVENT_TYPE,
{
'user_id': self.user.id,
'course_id': str(self.context_key),
'block_id': str(self.block_key),
'block_type': self.block_type,
'completion': self.completion,
Ian2012 marked this conversation as resolved.
Show resolved Hide resolved
'modified': self.modified,
'created': self.created,
}
)
51 changes: 51 additions & 0 deletions completion/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from datetime import datetime

from django.contrib import auth
from django.test.utils import override_settings
from edx_toggles.toggles.testutils import override_waffle_switch
from eventtracking import tracker
from django.test import TestCase
from eventtracking.django import DjangoTracker
import factory
from factory.django import DjangoModelFactory
from opaque_keys.edx.keys import UsageKey
Expand Down Expand Up @@ -120,3 +124,50 @@ def override_completion_switch(self, enabled):
"""
with override_waffle_switch(waffle.ENABLE_COMPLETION_TRACKING_SWITCH, enabled):
yield


IN_MEMORY_BACKEND_CONFIG = {
'mem': {
'ENGINE': 'completion.test_utils.InMemoryBackend'
}
}


class InMemoryBackend:
"""A backend that simply stores all events in memory"""

def __init__(self):
super().__init__() # lint-amnesty, pylint: disable=super-with-arguments
self.events = []

def send(self, event):
"""Store the event in a list"""
self.events.append(event)


@override_settings(
EVENT_TRACKING_BACKENDS=IN_MEMORY_BACKEND_CONFIG
)
class EventTrackingTestCase(TestCase):
"""
Supports capturing of emitted events in memory and inspecting them.

Each test gets a "clean slate" and can retrieve any events emitted during their execution.

"""

# Make this more robust to the addition of new events that the test doesn't care about.

def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments

self.recreate_tracker()

def recreate_tracker(self):
"""
Re-initialize the tracking system using updated django settings.

Use this if you make use of the @override_settings decorator to customize the tracker configuration.
"""
self.tracker = DjangoTracker()
tracker.register_tracker(self.tracker)
8 changes: 4 additions & 4 deletions completion/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from opaque_keys.edx.keys import CourseKey, UsageKey

from .. import models
from ..test_utils import CompletionSetUpMixin, UserFactory, submit_completions_for_testing
from ..test_utils import CompletionSetUpMixin, EventTrackingTestCase, UserFactory, submit_completions_for_testing


class PercentValidatorTestCase(TestCase):
Expand All @@ -29,7 +29,7 @@ def test_invalid_percent(self):
self.assertRaises(ValidationError, models.validate_percent, value)


class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
class SubmitCompletionTestCase(CompletionSetUpMixin, EventTrackingTestCase, TestCase):
"""
Test that BlockCompletion.objects.submit_completion has the desired
semantics.
Expand All @@ -41,7 +41,7 @@ def setUp(self):
self.set_up_completion()

def test_changed_value(self):
with self.assertNumQueries(6): # Get, update, 2 * savepoints, 2 * exists checks
with self.assertNumQueries(7): # 2 * Get, update, 2 * savepoints, 2 * exists checks
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are a little concerning, do you know where the extra queries are coming from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one additional for the obj.emit_tracking_log() method.

completion, isnew = models.BlockCompletion.objects.submit_completion(
user=self.user,
block_key=self.block_key,
Expand All @@ -53,7 +53,7 @@ def test_changed_value(self):
self.assertEqual(models.BlockCompletion.objects.count(), 1)

def test_unchanged_value(self):
with self.assertNumQueries(3): # Get + 2 * savepoints
with self.assertNumQueries(4): # 2 * Get + 2 * savepoints
completion, isnew = models.BlockCompletion.objects.submit_completion(
user=self.user,
block_key=self.block_key,
Expand Down
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ edx-drf-extensions>=1.11.0
edx-toggles>=1.2.0
pytz
XBlock>=1.2.2
event-tracking
47 changes: 46 additions & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#
# make upgrade
#
amqp==5.1.1
# via kombu
appdirs==1.4.4
# via fs
asgiref==3.7.2
Expand All @@ -12,8 +14,16 @@ astroid==2.15.6
# via
# pylint
# pylint-celery
backports-zoneinfo[tzdata]==0.2.1
# via
# celery
# kombu
billiard==4.1.0
# via celery
build==1.0.3
# via pip-tools
celery==5.3.4
# via event-tracking
certifi==2023.7.22
# via requests
cffi==1.15.1
Expand All @@ -26,13 +36,23 @@ charset-normalizer==3.2.0
# via requests
click==8.1.7
# via
# celery
# click-didyoumean
# click-log
# click-plugins
# click-repl
# code-annotations
# edx-django-utils
# edx-lint
# pip-tools
click-didyoumean==0.3.0
# via celery
click-log==0.4.0
# via edx-lint
click-plugins==1.1.1
# via celery
click-repl==0.3.0
# via celery
code-annotations==1.5.0
# via
# edx-lint
Expand Down Expand Up @@ -64,6 +84,7 @@ django==3.2.21
# edx-drf-extensions
# edx-i18n-tools
# edx-toggles
# event-tracking
django-crum==0.7.9
# via
# edx-django-utils
Expand All @@ -88,6 +109,7 @@ edx-django-utils==5.7.0
# via
# edx-drf-extensions
# edx-toggles
# event-tracking
edx-drf-extensions==8.9.2
# via -r requirements/base.in
edx-i18n-tools==1.1.0
Expand All @@ -102,6 +124,8 @@ edx-opaque-keys[django]==2.5.0
# edx-drf-extensions
edx-toggles==5.1.0
# via -r requirements/base.in
event-tracking==2.2.0
# via -r requirements/base.in
exceptiongroup==1.1.3
# via pytest
factory-boy==3.3.0
Expand Down Expand Up @@ -143,6 +167,8 @@ jinja2==3.1.2
# diff-cover
keyring==24.2.0
# via twine
kombu==5.3.2
# via celery
lazy-object-proxy==1.9.0
# via astroid
lxml==4.9.3
Expand Down Expand Up @@ -189,6 +215,8 @@ pluggy==1.3.0
# tox
polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.39
# via click-repl
psutil==5.9.5
# via edx-django-utils
py==1.11.0
Expand Down Expand Up @@ -223,7 +251,9 @@ pylint-plugin-utils==0.8.2
# pylint-celery
# pylint-django
pymongo==3.13.0
# via edx-opaque-keys
# via
# edx-opaque-keys
# event-tracking
pynacl==1.5.0
# via edx-django-utils
pyproject-hooks==1.0.0
Expand All @@ -238,6 +268,7 @@ pytest-django==4.5.2
# via -r requirements/test.in
python-dateutil==2.8.2
# via
# celery
# edx-drf-extensions
# faker
# freezegun
Expand All @@ -249,6 +280,7 @@ pytz==2023.3.post1
# -r requirements/base.in
# django
# djangorestframework
# event-tracking
# xblock
pyyaml==6.0.1
# via
Expand Down Expand Up @@ -276,6 +308,7 @@ six==1.16.0
# via
# edx-drf-extensions
# edx-lint
# event-tracking
# fs
# python-dateutil
# tox
Expand Down Expand Up @@ -317,14 +350,26 @@ typing-extensions==4.7.1
# edx-opaque-keys
# faker
# filelock
# kombu
# pylint
# rich
tzdata==2023.3
# via
# backports-zoneinfo
# celery
urllib3==2.0.4
# via
# requests
# twine
vine==5.0.0
# via
# amqp
# celery
# kombu
virtualenv==20.24.5
# via tox
wcwidth==0.2.6
# via prompt-toolkit
web-fragments==2.1.0
# via xblock
webob==1.8.7
Expand Down
Loading
Loading