From 49871598410ec472a9bbf6564bc51a924a0322eb Mon Sep 17 00:00:00 2001 From: Usman Khalid Date: Sat, 16 Jun 2018 00:23:31 +0500 Subject: [PATCH] Add helper methods for complete-on-view on CompletionService. --- AUTHORS | 1 + CHANGELOG.rst | 6 +++ completion/__init__.py | 2 +- completion/services.py | 24 +++++++++- completion/tests/test_services.py | 76 ++++++++++++++++++++++++++++++- 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5455625..47357c3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,4 +5,5 @@ Alessandro Roux Sofiya Semenova Jeremy Bowman Nimisha Asthagiri +Usman Khalid diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e71fcc6..5c07e7a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,12 @@ Change Log Unreleased ~~~~~~~~~~ +[0.1.7] - 2018-06-18 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* Added can_mark_block_complete_on_view() and blocks_to_mark_complete_on_view() +methods on CompletionService and renamed get_completion_by_viewing_delay_ms() +to get_complete_on_view_delay_ms(). + [0.1.6] - 2018-04-13 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Remove usage of deprecated CourseStructure api. diff --git a/completion/__init__.py b/completion/__init__.py index bf2e5d3..53031a1 100644 --- a/completion/__init__.py +++ b/completion/__init__.py @@ -5,6 +5,6 @@ from __future__ import unicode_literals -__version__ = '0.1.6' +__version__ = '0.1.7' default_app_config = 'completion.apps.CompletionAppConfig' # pylint: disable=invalid-name diff --git a/completion/services.py b/completion/services.py index 7740a42..2b7badc 100644 --- a/completion/services.py +++ b/completion/services.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, unicode_literals from django.conf import settings +from xblock.completable import XBlockCompletionMode from .models import BlockCompletion from . import waffle @@ -91,9 +92,28 @@ def vertical_is_complete(self, item): return False return True - def get_completion_by_viewing_delay_ms(self): + def get_complete_on_view_delay_ms(self): """ - Do not mark blocks complete-by-viewing until they have been visible for + Do not mark blocks complete-on-view until they have been visible for the returned amount of time, in milliseconds. Defaults to 5000. """ return getattr(settings, 'COMPLETION_BY_VIEWING_DELAY_MS', 5000) + + def can_mark_block_complete_on_view(self, block): + """ + Returns True if the xblock can be marked complete on view. + This is true of any non-customized, non-scorable, completable block. + """ + return ( + getattr(block, 'completion_mode', XBlockCompletionMode.COMPLETABLE) == XBlockCompletionMode.COMPLETABLE + and not getattr(block, 'has_custom_completion', False) + and not getattr(block, 'has_score', False) + ) + + def blocks_to_mark_complete_on_view(self, blocks): + """ + Returns a set of blocks which should be marked complete on view and haven't been yet. + """ + blocks = {block for block in blocks if self.can_mark_block_complete_on_view(block)} + completions = self.get_completions({block.location for block in blocks}) + return {block for block in blocks if completions.get(block.location, 0) < 1.0} diff --git a/completion/tests/test_services.py b/completion/tests/test_services.py index 99cdbda..0dc1117 100644 --- a/completion/tests/test_services.py +++ b/completion/tests/test_services.py @@ -7,7 +7,11 @@ import ddt from django.test import TestCase from django.test.utils import override_settings +from mock import Mock from opaque_keys.edx.keys import CourseKey, UsageKey +from xblock.completable import XBlockCompletionMode +from xblock.core import XBlock +from xblock.fields import ScopeIds from ..models import BlockCompletion from ..services import CompletionService @@ -80,6 +84,74 @@ def test_enabled_honors_waffle_switch(self, enabled): with self.override_completion_switch(enabled): self.assertEqual(self.completion_service.completion_tracking_enabled(), enabled) + @ddt.data( + (XBlockCompletionMode.COMPLETABLE, False, False, True), + (XBlockCompletionMode.COMPLETABLE, True, False, False), + (XBlockCompletionMode.COMPLETABLE, False, True, False), + (XBlockCompletionMode.AGGREGATOR, False, False, False), + (XBlockCompletionMode.EXCLUDED, False, False, False) + ) + @ddt.unpack + def test_can_mark_block_complete_on_view(self, mode, has_score, has_custom_completion, can_mark_complete): + block = XBlock(Mock(), scope_ids=Mock(spec=ScopeIds)) + block.completion_mode = mode + block.has_score = has_score + if has_custom_completion: + block.has_custom_completion = True + + self.assertEqual(self.completion_service.can_mark_block_complete_on_view(block), can_mark_complete) + + def test_blocks_to_mark_complete_on_view(self): + + completable_block_1 = XBlock(Mock(), scope_ids=Mock(spec=ScopeIds)) + completable_block_1.location = UsageKey.from_string("i4x://edX/100/a/1").replace(course_key=self.course_key) + completable_block_2 = XBlock(Mock(), scope_ids=Mock(spec=ScopeIds)) + completable_block_2.location = UsageKey.from_string("i4x://edX/100/a/2").replace(course_key=self.course_key) + aggregator_block = XBlock(Mock(), scope_ids=Mock(spec=ScopeIds)) + aggregator_block.location = UsageKey.from_string("i4x://edX/100/a/3").replace(course_key=self.course_key) + aggregator_block.completion_mode = XBlockCompletionMode.AGGREGATOR + + self.assertSetEqual(self.completion_service.blocks_to_mark_complete_on_view({}), set()) + + self.assertSetEqual( + self.completion_service.blocks_to_mark_complete_on_view({aggregator_block}), set() + ) + + self.assertSetEqual( + self.completion_service.blocks_to_mark_complete_on_view( + {completable_block_1, completable_block_2, aggregator_block} + ), + {completable_block_1, completable_block_2} + ) + + BlockCompletion.objects.submit_completion( + user=self.user, + course_key=self.course_key, + block_key=completable_block_2.location, + completion=1.0 + ) + + self.assertSetEqual( + self.completion_service.blocks_to_mark_complete_on_view( + {completable_block_1, completable_block_2, aggregator_block} + ), + {completable_block_1} + ) + + BlockCompletion.objects.submit_completion( + user=self.user, + course_key=self.course_key, + block_key=completable_block_1.location, + completion=1.0 + ) + + self.assertSetEqual( + self.completion_service.blocks_to_mark_complete_on_view( + {completable_block_1, completable_block_2, aggregator_block} + ), + set() + ) + @ddt.ddt class CompletionDelayTestCase(CompletionSetUpMixin, TestCase): @@ -89,7 +161,7 @@ class CompletionDelayTestCase(CompletionSetUpMixin, TestCase): """ @ddt.data(1, 1000, 0) - def test_get_completion_by_viewing_delay_ms(self, delay): + def test_get_complete_on_view_delay_ms(self, delay): service = CompletionService(self.user, self.course_key) with override_settings(COMPLETION_BY_VIEWING_DELAY_MS=delay): - self.assertEqual(service.get_completion_by_viewing_delay_ms(), delay) + self.assertEqual(service.get_complete_on_view_delay_ms(), delay)