-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
CompletionXBlockMixin + tests (#368)
CompletionXBlockMixin: * Mixin itself * Tests using hypothesis * Added (or rather restored) ability to run single test via tox
- Loading branch information
Showing
6 changed files
with
187 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
1.0.1 | ||
1.1.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
""" | ||
This module defines CompletableXBlockMixin and completion mode enumeration. | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
class XBlockCompletionMode(object): | ||
""" | ||
Enumeration for completion modes. | ||
""" | ||
COMPLETABLE = "completable" | ||
AGGREGATOR = "aggregator" | ||
EXCLUDED = "excluded" | ||
|
||
|
||
class CompletableXBlockMixin(object): | ||
""" | ||
This mixin sets attributes and provides helper method to integrate XBlock with Completion API. | ||
""" | ||
|
||
has_custom_completion = True | ||
completion_method = XBlockCompletionMode.COMPLETABLE | ||
|
||
# To read more on the debate about using the terms percent vs ratio, see: | ||
# https://openedx.atlassian.net/wiki/spaces/OpenDev/pages/245465398/Naming+with+Percent+or+Ratio | ||
def emit_completion(self, completion_percent): | ||
""" | ||
Emits completion event through Completion API. | ||
Unlike grading API, calling this method allows completion to go down - i.e. emitting a value of 0.0 on | ||
a previously completed block indicates that it is no longer considered complete. | ||
Arguments: | ||
completion_percent (float): Completion in range [0.0; 1.0] (inclusive), where 0.0 means the block | ||
is not completed, 1.0 means the block is fully completed. | ||
Returns: | ||
None | ||
""" | ||
if not self.has_custom_completion or self.completion_method != 'completable': | ||
raise AttributeError( | ||
"Using `emit_completion` requires `has_custom_completion == True` (was {}) " | ||
"and `completion_method == 'completable'` (was {})".format( | ||
self.has_custom_completion, self.completion_method | ||
) | ||
) | ||
|
||
if completion_percent is None or not 0.0 <= completion_percent <= 1.0: | ||
raise ValueError("Completion percent must be in [0.0; 1.0] interval, {} given".format(completion_percent)) | ||
|
||
self.runtime.publish( | ||
self, | ||
'completion', | ||
{'completion': completion_percent}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
""" | ||
Tests of the CompletableXBlockMixin. | ||
""" | ||
from __future__ import absolute_import, unicode_literals | ||
|
||
import math | ||
from unittest import TestCase | ||
|
||
import mock | ||
from hypothesis import given, example | ||
import hypothesis.strategies as strategies | ||
|
||
from xblock.core import XBlock | ||
from xblock.fields import ScopeIds | ||
from xblock.runtime import Runtime | ||
from xblock.completable import CompletableXBlockMixin, XBlockCompletionMode | ||
|
||
|
||
class CompletableXBlockMixinTest(TestCase): | ||
""" | ||
Tests for CompletableXBlockMixin. | ||
""" | ||
class TestBuddyXBlock(XBlock, CompletableXBlockMixin): | ||
""" | ||
Simple XBlock extending CompletableXBlockMixin. | ||
""" | ||
|
||
class TestIllegalCustomCompletionAttrXBlock(XBlock, CompletableXBlockMixin): | ||
""" | ||
XBlock extending CompletableXBlockMixin using illegal `has_custom_completion` attribute. | ||
""" | ||
has_custom_completion = False | ||
|
||
class TestIllegalCompletionMethodAttrXBlock(XBlock, CompletableXBlockMixin): | ||
""" | ||
XBlock extending CompletableXBlockMixin using illegal `completion_method` attribute. | ||
""" | ||
completion_method = "something_else" | ||
|
||
def _make_block(self, runtime=None, block_type=None): | ||
""" | ||
Creates a test block. | ||
""" | ||
block_type = block_type if block_type else self.TestBuddyXBlock | ||
runtime = runtime if runtime else mock.Mock(spec=Runtime) | ||
scope_ids = ScopeIds("user_id", "test_buddy", "def_id", "usage_id") | ||
return block_type(runtime=runtime, scope_ids=scope_ids) | ||
|
||
def test_has_custom_completion_property(self): | ||
""" | ||
Test `has_custom_completion` property is set by mixin. | ||
""" | ||
block = self._make_block() | ||
self.assertTrue(block.has_custom_completion) | ||
self.assertTrue(getattr(block, 'has_custom_completion', False)) | ||
|
||
def test_completion_method_property(self): | ||
""" | ||
Test `completion_method` property is set by mixin. | ||
""" | ||
block = self._make_block() | ||
self.assertEqual(block.completion_method, XBlockCompletionMode.COMPLETABLE) | ||
self.assertEqual(getattr(block, 'completion_method', ""), XBlockCompletionMode.COMPLETABLE) | ||
|
||
@given(strategies.floats()) | ||
def test_emit_completion_illegal_custom_completion(self, any_completion): | ||
""" | ||
Test `emit_completion` raises exception when called on a XBlock with illegal `has_custom_completion` value. | ||
""" | ||
runtime_mock = mock.Mock(spec=Runtime) | ||
illegal_custom_completion_block = self._make_block(runtime_mock, self.TestIllegalCustomCompletionAttrXBlock) | ||
with self.assertRaises(AttributeError): | ||
illegal_custom_completion_block.emit_completion(any_completion) | ||
|
||
@given(strategies.floats()) | ||
def test_emit_completion_completion_method(self, any_completion): | ||
""" | ||
Test `emit_completion` raises exception when called on a XBlock with illegal `completion_method` value. | ||
""" | ||
runtime_mock = mock.Mock(spec=Runtime) | ||
illegal_completion_method_block = self._make_block(runtime_mock, self.TestIllegalCompletionMethodAttrXBlock) | ||
with self.assertRaises(AttributeError): | ||
illegal_completion_method_block.emit_completion(any_completion) | ||
|
||
@given(strategies.floats(min_value=0.0, max_value=1.0)) | ||
@example(1.0) | ||
@example(0.0) | ||
def test_emit_completion_emits_event(self, valid_completion_percent): | ||
""" | ||
Test `emit_completion` emits completion events when passed a valid argument. | ||
Given a valid completion percent | ||
When emit_completion is called | ||
Then runtime.publish is called with expected arguments | ||
""" | ||
runtime_mock = mock.Mock(spec=Runtime) | ||
block = self._make_block(runtime_mock) | ||
block.emit_completion(valid_completion_percent) | ||
|
||
runtime_mock.publish.assert_called_once_with(block, "completion", {"completion": valid_completion_percent}) | ||
|
||
@given(strategies.floats().filter(lambda x: math.isnan(x) or x < 0.0 or x > 1.0)) | ||
@example(None) | ||
@example(float('+inf')) | ||
@example(float('-inf')) | ||
def test_emit_completion_raises_assertion_error_if_invalid(self, invalid_completion_percent): | ||
""" | ||
Test `emit_completion` raises exception when passed an invalid argument. | ||
Given an invalid completion percent | ||
* Less than 0.0 | ||
* Greater than 1.0 | ||
* Positive or negative infinity | ||
* NaN | ||
When emit_completion is called | ||
Then value error is thrown | ||
""" | ||
runtime_mock = mock.Mock(spec=Runtime) | ||
block = self._make_block(runtime_mock) | ||
with self.assertRaises(ValueError): | ||
self.assertRaises(block.emit_completion(invalid_completion_percent)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters