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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
20
lms/djangoapps/completion/waffle.py
Normal file
20
lms/djangoapps/completion/waffle.py
Normal 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: ')
|
||||
Reference in New Issue
Block a user