Skip to content
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
3 changes: 3 additions & 0 deletions lms/djangoapps/completion/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ class CompletionAppConfig(AppConfig):
"""
name = 'lms.djangoapps.completion'
verbose_name = 'Completion'

def ready(self):
from . import handlers # pylint: disable=unused-variable
36 changes: 36 additions & 0 deletions lms/djangoapps/completion/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
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,
)
120 changes: 120 additions & 0 deletions lms/djangoapps/completion/tests/test_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
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
)
6 changes: 3 additions & 3 deletions lms/djangoapps/completion/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ class SubmitCompletionTestCase(CompletionSetUpMixin, TestCase):
"""
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)
_overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True)
_overrider.__enter__()
self.addCleanup(_overrider.__exit__, None, None, None)
self.set_up_completion()

def test_changed_value(self):
Expand Down
76 changes: 22 additions & 54 deletions lms/djangoapps/courseware/tests/test_module_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def complete(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.

New code should use the completion event instead.
"""
return self.runtime.publish(self, 'progress', {})
Expand Down Expand Up @@ -425,6 +426,7 @@ def test_hash_resource(self):


@attr(shard=1)
@ddt.ddt
class TestHandleXBlockCallback(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test the handle_xblock_callback function
Expand Down Expand Up @@ -606,91 +608,57 @@ def test_score_without_student_state(self):
self.assertEquals(student_module.grade, 0.75)
self.assertEquals(student_module.max_grade, 1)

@ddt.data(
('complete', {'completion': 0.625}),
('progress', {}),
)
@ddt.unpack
@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_completion_event_with_completion_disabled(self):
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):
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({}),
data=json.dumps(data),
content_type='application/json',
)
request.user = self.mock_user
with self.assertRaises(Http404):
response = render.handle_xblock_callback(
render.handle_xblock_callback(
request,
unicode(course.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')
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):
course = CourseFactory.create()
block = ItemFactory.create(category='comp', parent=course)
request = self.request_factory.post(
'/',
data=json.dumps({}),
data=json.dumps(data),
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)),
'progress',
signal,
'',
)
self.assertEqual(response.status_code, 200)
completion = BlockCompletion.objects.get(block_key=block.scope_ids.usage_id)
self.assertEqual(completion.completion, 1.0)
self.assertEqual(response.status_code, 200)
completion = BlockCompletion.objects.get(block_key=block.scope_ids.usage_id)
self.assertEqual(completion.completion, expected_completion)

@XBlock.register_temp_plugin(StubCompletableXBlock, identifier='comp')
def test_skip_handlers_for_masquerading_staff(self):
Expand Down
4 changes: 4 additions & 0 deletions lms/djangoapps/grades/signals/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
'modified', # A datetime indicating when the database representation of
# this the problem score was saved.
'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.
]
)

Expand All @@ -49,6 +51,8 @@
'modified', # A datetime indicating when the database representation of
# this the problem score was saved.
'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.
]
)

Expand Down