Add handler to receive completion events

* Submit a completion when receiving a completion event from an XBlock.
* Handle legacy progress events.
* Convert handler to use a dispatch dict instead of an if-else chain.
* Extract masquerade checking from individual handlers.
* Gate submit_completion on waffle switch
* 404 on handler views when trying to submit completion without waffle
switch enabled.

OC-3087

Disallow calling submit_completion when waffle flag is disabled.

Add tests that trying to publish completion errors.
This commit is contained in:
J. Cliff Dyer
2017-09-26 11:06:22 -04:00
parent c12923838f
commit 94d05bceb5
5 changed files with 318 additions and 36 deletions

View File

@@ -12,6 +12,7 @@ from model_utils.models import TimeStampedModel
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.xmodule_django.models import CourseKeyField, UsageKeyField
from . import waffle
# pylint: disable=ungrouped-imports
try:
@@ -52,7 +53,8 @@ class BlockCompletionManager(models.Manager):
Return Value:
(BlockCompletion, bool): A tuple comprising the created or updated
BlockCompletion object and a boolean value indicating whether the value
BlockCompletion object and a boolean value indicating whether the
object was newly created by this call.
Raises:
@@ -84,17 +86,23 @@ class BlockCompletionManager(models.Manager):
"block_key must be an instance of `opaque_keys.edx.keys.UsageKey`. Got {}".format(type(block_key))
)
obj, isnew = self.get_or_create(
user=user,
course_key=course_key,
block_type=block_type,
block_key=block_key,
defaults={'completion': completion},
)
if not isnew and obj.completion != completion:
obj.completion = completion
obj.full_clean()
obj.save()
if waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING):
obj, isnew = self.get_or_create(
user=user,
course_key=course_key,
block_type=block_type,
block_key=block_key,
defaults={'completion': completion},
)
if not isnew and obj.completion != completion:
obj.completion = completion
obj.full_clean()
obj.save()
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, isnew

View File

@@ -2,6 +2,8 @@
Test models, managers, and validators.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from django.core.exceptions import ValidationError
from django.test import TestCase
from opaque_keys.edx.keys import UsageKey
@@ -9,6 +11,7 @@ from opaque_keys.edx.keys import UsageKey
from student.tests.factories import UserFactory
from .. import models
from .. import waffle
class PercentValidatorTestCase(TestCase):
@@ -24,13 +27,8 @@ class PercentValidatorTestCase(TestCase):
self.assertRaises(ValidationError, models.validate_percent, value)
class SubmitCompletionTestCase(TestCase):
"""
Test that BlockCompletion.objects.submit_completion has the desired
semantics.
"""
def setUp(self):
super(SubmitCompletionTestCase, self).setUp()
class CompletionSetUpMixin(object):
def set_up_completion(self):
self.user = UserFactory()
self.block_key = UsageKey.from_string(u'block-v1:edx+test+run+type@video+block@doggos')
self.completion = models.BlockCompletion.objects.create(
@@ -41,6 +39,19 @@ class SubmitCompletionTestCase(TestCase):
completion=0.5,
)
class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
"""
Test that BlockCompletion.objects.submit_completion has the desired
semantics.
"""
def setUp(self):
super(SubmitCompletionTestCase, self).setUp()
self._overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True)
self._overrider.__enter__()
self.addCleanup(self._overrider.__exit__, None, None, None)
self.set_up_completion()
def test_changed_value(self):
with self.assertNumQueries(4): # Get, update, 2 * savepoints
completion, isnew = models.BlockCompletion.objects.submit_completion(
@@ -102,3 +113,32 @@ class SubmitCompletionTestCase(TestCase):
completion = models.BlockCompletion.objects.get(user=self.user, block_key=self.block_key)
self.assertEqual(completion.completion, 0.5)
self.assertEqual(models.BlockCompletion.objects.count(), 1)
class CompletionDisabledTestCase(CompletionSetUpMixin, TestCase):
@classmethod
def setUpClass(cls):
super(CompletionDisabledTestCase, cls).setUpClass()
cls.overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, False)
cls.overrider.__enter__()
@classmethod
def tearDownClass(cls):
cls.overrider.__exit__(None, None, None)
super(CompletionDisabledTestCase, cls).tearDownClass()
def setUp(self):
super(CompletionDisabledTestCase, self).setUp()
self.set_up_completion()
def test_cannot_call_submit_completion(self):
self.assertEqual(models.BlockCompletion.objects.count(), 1)
with self.assertRaises(RuntimeError):
models.BlockCompletion.objects.submit_completion(
user=self.user,
course_key=self.block_key.course_key,
block_key=self.block_key,
completion=0.9,
)
self.assertEqual(models.BlockCompletion.objects.count(), 1)

View File

@@ -0,0 +1,20 @@
"""
This module contains various configuration settings via
waffle switches for the completion app.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace
# Namespace
WAFFLE_NAMESPACE = 'completion'
# Switches
ENABLE_COMPLETION_TRACKING = 'enable_completion_tracking'
def waffle():
"""
Returns the namespaced, cached, audited Waffle class for completion.
"""
return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='completion: ')