Commit 2cadbbad by Cliff Dyer Committed by GitHub

Merge pull request #16186 from open-craft/cliff/complete-scored-blocks

Complete scored blocks
parents a9448876 69271d04
...@@ -12,3 +12,6 @@ class CompletionAppConfig(AppConfig): ...@@ -12,3 +12,6 @@ class CompletionAppConfig(AppConfig):
""" """
name = 'lms.djangoapps.completion' name = 'lms.djangoapps.completion'
verbose_name = 'Completion' verbose_name = 'Completion'
def ready(self):
from . import handlers # pylint: disable=unused-variable
"""
Signal handlers to trigger completion updates.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from django.contrib.auth.models import User
from django.dispatch import receiver
from opaque_keys.edx.keys import CourseKey, UsageKey
from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from .models import BlockCompletion
from . import waffle
@receiver(PROBLEM_WEIGHTED_SCORE_CHANGED)
def scorable_block_completion(sender, **kwargs): # pylint: disable=unused-argument
"""
When a problem is scored, submit a new BlockCompletion for that block.
"""
if not waffle.waffle().is_enabled(waffle.ENABLE_COMPLETION_TRACKING):
return
user = User.objects.get(id=kwargs['user_id'])
course_key = CourseKey.from_string(kwargs['course_id'])
block_key = UsageKey.from_string(kwargs['usage_id'])
if kwargs.get('score_deleted'):
completion = 0.0
else:
completion = 1.0
BlockCompletion.objects.submit_completion(
user=user,
course_key=course_key,
block_key=block_key,
completion=completion,
)
"""
Test signal handlers.
"""
from datetime import datetime
import ddt
from django.test import TestCase
from mock import patch
from opaque_keys.edx.keys import CourseKey
from pytz import utc
import six
from lms.djangoapps.grades.signals.signals import PROBLEM_WEIGHTED_SCORE_CHANGED
from student.tests.factories import UserFactory
from .. import handlers
from ..models import BlockCompletion
from .. import waffle
class CompletionHandlerMixin(object):
"""
Common functionality for completion handler tests.
"""
def override_waffle_switch(self, override):
"""
Override the setting of the ENABLE_COMPLETION_TRACKING waffle switch
for the course of the test.
Parameters:
override (bool): True if tracking should be enabled.
"""
_waffle_overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, override)
_waffle_overrider.__enter__()
self.addCleanup(_waffle_overrider.__exit__, None, None, None)
@ddt.ddt
class ScorableCompletionHandlerTestCase(CompletionHandlerMixin, TestCase):
"""
Test the signal handler
"""
def setUp(self):
super(ScorableCompletionHandlerTestCase, self).setUp()
self.user = UserFactory.create()
self.course_key = CourseKey.from_string("course-v1:a+valid+course")
self.block_key = self.course_key.make_usage_key(block_type="video", block_id="mah-video")
self.override_waffle_switch(True)
@ddt.data(
({'score_deleted': True}, 0.0),
({'score_deleted': False}, 1.0),
({}, 1.0),
)
@ddt.unpack
def test_handler_submits_completion(self, params, expected_completion):
handlers.scorable_block_completion(
sender=self,
user_id=self.user.id,
course_id=six.text_type(self.course_key),
usage_id=six.text_type(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
modified=datetime.utcnow().replace(tzinfo=utc),
score_db_table='submissions',
**params
)
completion = BlockCompletion.objects.get(user=self.user, course_key=self.course_key, block_key=self.block_key)
self.assertEqual(completion.completion, expected_completion)
def test_signal_calls_handler(self):
user = UserFactory.create()
course_key = CourseKey.from_string("course-v1:a+valid+course")
block_key = course_key.make_usage_key(block_type="video", block_id="mah-video")
with patch('lms.djangoapps.completion.handlers.scorable_block_completion') as mock_handler:
PROBLEM_WEIGHTED_SCORE_CHANGED.send_robust(
sender=self,
user_id=user.id,
course_id=six.text_type(course_key),
usage_id=six.text_type(block_key),
weighted_earned=0.0,
weighted_possible=3.0,
modified=datetime.utcnow().replace(tzinfo=utc),
score_db_table='submissions',
)
mock_handler.assert_called()
class DisabledCompletionHandlerTestCase(CompletionHandlerMixin, TestCase):
"""
Test that disabling the ENABLE_COMPLETION_TRACKING waffle switch prevents
the signal handler from submitting a completion.
"""
def setUp(self):
super(DisabledCompletionHandlerTestCase, self).setUp()
self.user = UserFactory.create()
self.course_key = CourseKey.from_string("course-v1:a+valid+course")
self.block_key = self.course_key.make_usage_key(block_type="video", block_id="mah-video")
self.override_waffle_switch(False)
def test_disabled_handler_does_not_submit_completion(self):
handlers.scorable_block_completion(
sender=self,
user_id=self.user.id,
course_id=six.text_type(self.course_key),
usage_id=six.text_type(self.block_key),
weighted_earned=0.0,
weighted_possible=3.0,
modified=datetime.utcnow().replace(tzinfo=utc),
score_db_table='submissions',
)
with self.assertRaises(BlockCompletion.DoesNotExist):
BlockCompletion.objects.get(
user=self.user,
course_key=self.course_key,
block_key=self.block_key
)
...@@ -47,9 +47,9 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase): ...@@ -47,9 +47,9 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
""" """
def setUp(self): def setUp(self):
super(SubmitCompletionTestCase, self).setUp() super(SubmitCompletionTestCase, self).setUp()
self._overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True)
self._overrider.__enter__() _overrider.__enter__()
self.addCleanup(self._overrider.__exit__, None, None, None) self.addCleanup(_overrider.__exit__, None, None, None)
self.set_up_completion() self.set_up_completion()
def test_changed_value(self): def test_changed_value(self):
......
...@@ -136,6 +136,7 @@ class StubCompletableXBlock(XBlock): ...@@ -136,6 +136,7 @@ class StubCompletableXBlock(XBlock):
def progress(self, json_data, suffix): # pylint: disable=unused-argument def progress(self, json_data, suffix): # pylint: disable=unused-argument
""" """
Mark the block as complete using the deprecated progress interface. Mark the block as complete using the deprecated progress interface.
New code should use the completion event instead. New code should use the completion event instead.
""" """
return self.runtime.publish(self, 'progress', {}) return self.runtime.publish(self, 'progress', {})
...@@ -425,6 +426,7 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase): ...@@ -425,6 +426,7 @@ class ModuleRenderTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
@attr(shard=1) @attr(shard=1)
@ddt.ddt
class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCase): class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
""" """
Test the handle_xblock_callback function Test the handle_xblock_callback function
...@@ -606,78 +608,44 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -606,78 +608,44 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
self.assertEquals(student_module.grade, 0.75) self.assertEquals(student_module.grade, 0.75)
self.assertEquals(student_module.max_grade, 1) self.assertEquals(student_module.max_grade, 1)
@ddt.data(
('complete', {'completion': 0.625}),
('progress', {}),
)
@ddt.unpack
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp') @XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_completion_event_with_completion_disabled(self): def test_completion_events_with_completion_disabled(self, signal, data):
with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, False):
course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course)
request = self.request_factory.post(
'/',
data=json.dumps({'completion': 0.625}),
content_type='application/json',
)
request.user = self.mock_user
with self.assertRaises(Http404):
result = render.handle_xblock_callback(
request,
unicode(course.id),
quote_slashes(unicode(block.scope_ids.usage_id)),
'complete',
'',
)
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_completion_event(self):
with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True):
course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course)
request = self.request_factory.post(
'/',
data=json.dumps({'completion': 0.625}),
content_type='application/json',
)
request.user = self.mock_user
response = render.handle_xblock_callback(
request,
unicode(course.id),
quote_slashes(unicode(block.scope_ids.usage_id)),
'complete',
'',
)
self.assertEqual(response.status_code, 200)
completion = BlockCompletion.objects.get(block_key=block.scope_ids.usage_id)
self.assertEqual(completion.completion, 0.625)
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_progress_event_with_completion_disabled(self):
with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, False): with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, False):
course = CourseFactory.create() course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course) block = ItemFactory.create(category='comp', parent=course)
request = self.request_factory.post( request = self.request_factory.post(
'/', '/',
data=json.dumps({}), data=json.dumps(data),
content_type='application/json', content_type='application/json',
) )
request.user = self.mock_user request.user = self.mock_user
with self.assertRaises(Http404): with self.assertRaises(Http404):
response = render.handle_xblock_callback( render.handle_xblock_callback(
request, request,
unicode(course.id), unicode(course.id),
quote_slashes(unicode(block.scope_ids.usage_id)), quote_slashes(unicode(block.scope_ids.usage_id)),
'progress', signal,
'', '',
) )
self.assertEqual(response.status_code, 404)
raise Http404
@ddt.data(
('complete', {'completion': 0.625}, 0.625),
('progress', {}, 1.0),
)
@ddt.unpack
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp') @XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_progress_event(self): def test_completion_events(self, signal, data, expected_completion):
with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True): with completion_waffle.waffle().override(completion_waffle.ENABLE_COMPLETION_TRACKING, True):
course = CourseFactory.create() course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course) block = ItemFactory.create(category='comp', parent=course)
request = self.request_factory.post( request = self.request_factory.post(
'/', '/',
data=json.dumps({}), data=json.dumps(data),
content_type='application/json', content_type='application/json',
) )
request.user = self.mock_user request.user = self.mock_user
...@@ -685,12 +653,12 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas ...@@ -685,12 +653,12 @@ class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCas
request, request,
unicode(course.id), unicode(course.id),
quote_slashes(unicode(block.scope_ids.usage_id)), quote_slashes(unicode(block.scope_ids.usage_id)),
'progress', signal,
'', '',
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
completion = BlockCompletion.objects.get(block_key=block.scope_ids.usage_id) completion = BlockCompletion.objects.get(block_key=block.scope_ids.usage_id)
self.assertEqual(completion.completion, 1.0) self.assertEqual(completion.completion, expected_completion)
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp') @XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_skip_handlers_for_masquerading_staff(self): def test_skip_handlers_for_masquerading_staff(self):
......
...@@ -26,6 +26,8 @@ PROBLEM_RAW_SCORE_CHANGED = Signal( ...@@ -26,6 +26,8 @@ PROBLEM_RAW_SCORE_CHANGED = Signal(
'modified', # A datetime indicating when the database representation of 'modified', # A datetime indicating when the database representation of
# this the problem score was saved. # this the problem score was saved.
'score_db_table', # The database table that houses the score that changed. 'score_db_table', # The database table that houses the score that changed.
'score_deleted', # Boolean indicating whether the score changed due to
# the user state being deleted.
] ]
) )
...@@ -49,6 +51,8 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal( ...@@ -49,6 +51,8 @@ PROBLEM_WEIGHTED_SCORE_CHANGED = Signal(
'modified', # A datetime indicating when the database representation of 'modified', # A datetime indicating when the database representation of
# this the problem score was saved. # this the problem score was saved.
'score_db_table', # The database table that houses the score that changed. 'score_db_table', # The database table that houses the score that changed.
'score_deleted', # Boolean indicating whether the score changed due to
# the user state being deleted.
] ]
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment