From 5e5d897cdce458c45f1a17d888a8acbd46a198a7 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 24 Feb 2016 18:33:23 +0100 Subject: [PATCH 1/4] Fix: Make sure collecting "step" children of Problem Builder (Mentoring) blocks works in both Apros and the LMS. --- problem_builder/public/js/mentoring.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/problem_builder/public/js/mentoring.js b/problem_builder/public/js/mentoring.js index 04840e0c..3eb47f66 100644 --- a/problem_builder/public/js/mentoring.js +++ b/problem_builder/public/js/mentoring.js @@ -8,7 +8,9 @@ function MentoringBlock(runtime, element) { var attemptsTemplate = _.template($('#xblock-attempts-template').html()); var data = $('.mentoring', element).data(); var children = runtime.children(element); - var steps = runtime.children(element).filter(function(c) { return c.element.className.indexOf('assessment_step_view') > -1; }); + var steps = runtime.children(element).filter(function(c) { + return $(c.element).attr("class").indexOf('assessment_step_view') > -1; + }); var step = data.step; var mentoring = { From 79bdff7654bdb9e11c26d2d3903a065a23e380bd Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Wed, 24 Feb 2016 18:40:18 +0100 Subject: [PATCH 2/4] Introduce instance-wide setting that specifies if previous answers to MCQs are shown when users revisit them. --- problem_builder/mentoring.py | 49 ++++++++++++++++++---- problem_builder/public/js/questionnaire.js | 37 ++++++++-------- 2 files changed, 62 insertions(+), 24 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 3c11826b..943287a9 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -63,6 +63,10 @@ 'locations': ['public/themes/lms.css'] } +_default_options_config = { + 'pb_mcq_hide_previous_answer': False +} + # Make '_' a no-op so we can scrape strings def _(text): @@ -121,6 +125,7 @@ class BaseMentoringBlock( icon_class = 'problem' block_settings_key = 'mentoring' theme_key = 'theme' + options_key = 'options' @property def url_name(self): @@ -156,18 +161,28 @@ def get_content_titles(self): return [self.display_name] return [] - def get_theme(self): + def get_settings(self, settings_key, default): """ - Gets theme settings from settings service. Falls back to default (LMS) theme - if settings service is not available, xblock theme settings are not set or does - contain mentoring theme settings. + Get settings identified by `settings_key` from settings service. + + Fall back on `default` if settings service is unavailable + or settings have not been customized. """ settings_service = self.runtime.service(self, "settings") if settings_service: xblock_settings = settings_service.get_settings_bucket(self) - if xblock_settings and self.theme_key in xblock_settings: - return xblock_settings[self.theme_key] - return _default_theme_config + if xblock_settings and settings_key in xblock_settings: + return xblock_settings[settings_key] + return default + + def get_theme(self): + """ + Get theme settings for this block from settings service. + + Fall back on default (LMS) theme if settings service is not available, + or theme has not been customized. + """ + return self.get_settings(self.theme_key, _default_theme_config) def include_theme_files(self, fragment): theme = self.get_theme() @@ -175,6 +190,18 @@ def include_theme_files(self, fragment): for theme_file in theme_files: fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file)) + def get_options(self): + """ + Get options settings for this block from settings service. + + Fall back on default options if settings service is not available + or options have not been customized. + """ + return self.get_settings(self.options_key, _default_options_config) + + def get_option(self, option): + return self.get_options()[option] + @XBlock.json_handler def view(self, data, suffix=''): """ @@ -368,6 +395,8 @@ def score(self): return Score(score, int(round(score * 100)), correct, incorrect, partially_correct) def student_view(self, context): + from .mcq import MCQBlock # Import here to avoid circular dependency + # Migrate stored data if necessary self.migrate_fields() @@ -379,6 +408,8 @@ def student_view(self, context): fragment = Fragment() child_content = u"" + mcq_hide_previous_answer = self.get_option('pb_mcq_hide_previous_answer') + for child_id in self.children: child = self.runtime.get_block(child_id) if child is None: # child should not be None but it can happen due to bugs or permission issues @@ -388,6 +419,10 @@ def student_view(self, context): if self.is_assessment and isinstance(child, QuestionMixin): child_fragment = child.render('assessment_step_view', context) else: + if mcq_hide_previous_answer and isinstance(child, MCQBlock): + context['hide_prev_answer'] = True + else: + context['hide_prev_answer'] = False child_fragment = child.render('mentoring_view', context) except NoSuchViewError: if child.scope_ids.block_type == 'html' and getattr(self.runtime, 'is_author_mode', False): diff --git a/problem_builder/public/js/questionnaire.js b/problem_builder/public/js/questionnaire.js index 4a4a8b27..e0cb4827 100644 --- a/problem_builder/public/js/questionnaire.js +++ b/problem_builder/public/js/questionnaire.js @@ -122,7 +122,7 @@ function MCQBlock(runtime, element) { var mentoring = this.mentoring; var messageView = MessageView(element, mentoring); - + if (result.message) { var msg = '
' + result.message + '
' + '
'; @@ -138,24 +138,27 @@ function MCQBlock(runtime, element) { var choiceResultDOM = $('.choice-result', choiceDOM); var choiceTipsDOM = $('.choice-tips', choiceDOM); - if (result.status === "correct" && choiceInputDOM.val() === result.submission) { - choiceDOM.addClass('correct'); - choiceResultDOM.addClass('checkmark-correct icon-ok fa-check'); - } - else if (choiceInputDOM.val() === result.submission || _.isNull(result.submission)) { - choiceDOM.addClass('incorrect'); - choiceResultDOM.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); - } - - if (result.tips && choiceInputDOM.val() === result.submission) { - mentoring.setContent(choiceTipsDOM, result.tips); - } + if (choiceInputDOM.prop('checked')) { // We're showing previous answers, + // so go ahead and display results as well + if (result.status === "correct" && choiceInputDOM.val() === result.submission) { + choiceDOM.addClass('correct'); + choiceResultDOM.addClass('checkmark-correct icon-ok fa-check'); + } + else if (choiceInputDOM.val() === result.submission || _.isNull(result.submission)) { + choiceDOM.addClass('incorrect'); + choiceResultDOM.addClass('checkmark-incorrect icon-exclamation fa-exclamation'); + } - choiceResultDOM.off('click').on('click', function() { - if (choiceTipsDOM.html() !== '') { - messageView.showMessage(choiceTipsDOM); + if (result.tips && choiceInputDOM.val() === result.submission) { + mentoring.setContent(choiceTipsDOM, result.tips); } - }); + + choiceResultDOM.off('click').on('click', function() { + if (choiceTipsDOM.html() !== '') { + messageView.showMessage(choiceTipsDOM); + } + }); + } }); if (_.isNull(result.submission)) { From 86e2483573c26b7445e1a913f5705e8e69b44bfe Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Thu, 25 Feb 2016 15:40:33 +0100 Subject: [PATCH 3/4] Update tests. - Fix existing tests. - Add unit tests for code that deals with instance-wide options. - Add integration test for scenario where previous answers for MCQs are configured to be hidden. --- .../tests/integration/test_mentoring.py | 58 +++++++++ .../tests/integration/test_titles.py | 6 +- .../tests/unit/test_problem_builder.py | 115 ++++++++++++++---- 3 files changed, 151 insertions(+), 28 deletions(-) diff --git a/problem_builder/tests/integration/test_mentoring.py b/problem_builder/tests/integration/test_mentoring.py index de71e82a..7a937373 100644 --- a/problem_builder/tests/integration/test_mentoring.py +++ b/problem_builder/tests/integration/test_mentoring.py @@ -131,6 +131,11 @@ def _assert_feedback_hidden(self, questionnaire, choice_index): self.assertNotIn('checkmark-correct', choice_result_classes) self.assertNotIn('checkmark-incorrect', choice_result_classes) + def _assert_not_checked(self, questionnaire, choice_index): + choice = self._get_choice(questionnaire, choice_index) + choice_input = choice.find_element_by_css_selector('input') + self.assertFalse(choice_input.is_selected()) + def _standard_filling(self, answer, mcq, mrq, rating): answer.send_keys('This is the answer') self.click_choice(mcq, "Yes") @@ -164,6 +169,32 @@ def _standard_checks(self, answer, mcq, mrq, rating, messages): self.assertTrue(messages.is_displayed()) self.assertEqual(messages.text, "FEEDBACK\nNot done yet") + def _feedback_customized_checks(self, answer, mcq, mrq, rating, messages): + # Long answer: Previous answer and feedback visible + self.assertEqual(answer.get_attribute('value'), 'This is the answer') + # MCQ: Previous answer and feedback hidden + for i in range(3): + self._assert_feedback_hidden(mcq, i) + self._assert_not_checked(mcq, i) + # MRQ: Previous answer and feedback visible + self._assert_feedback_showed( + mrq, 0, "This is something everyone has to like about this MRQ", + click_choice_result=True + ) + self._assert_feedback_showed( + mrq, 1, "This is something everyone has to like about beauty", + click_choice_result=True, success=False + ) + self._assert_feedback_showed(mrq, 2, "This MRQ is indeed very graceful", click_choice_result=True) + self._assert_feedback_showed(mrq, 3, "Nah, there aren't any!", click_choice_result=True, success=False) + # Rating: Previous answer and feedback hidden + for i in range(5): + self._assert_feedback_hidden(rating, i) + self._assert_not_checked(rating, i) + # Messages + self.assertTrue(messages.is_displayed()) + self.assertEqual(messages.text, "FEEDBACK\nNot done yet") + def reload_student_view(self): # Load another page (the home page), then go back to the page we want. This is the only reliable way to reload. self.browser.get(self.live_server_url + '/') @@ -218,6 +249,33 @@ def test_persists_feedback_on_page_reload(self): self.click_choice(mrq, "Its elegance") self.assertTrue(submit.is_enabled()) + def test_does_not_persist_mcq_feedback_on_page_reload_if_disabled(self): + with mock.patch("problem_builder.mentoring.MentoringBlock.get_options") as patched_options: + patched_options.return_value = {'pb_mcq_hide_previous_answer': True} + mentoring = self.load_scenario("feedback_persistence.xml") + answer, mcq, mrq, rating = self._get_controls(mentoring) + messages = self._get_messages_element(mentoring) + + self._standard_filling(answer, mcq, mrq, rating) + self.click_submit(mentoring) + self._standard_checks(answer, mcq, mrq, rating, messages) + + # now, reload the page and see if previous answers and results for MCQs are hidden + mentoring = self.reload_student_view() + answer, mcq, mrq, rating = self._get_controls(mentoring) + messages = self._get_messages_element(mentoring) + submit = mentoring.find_element_by_css_selector('.submit input.input-main') + + self._feedback_customized_checks(answer, mcq, mrq, rating, messages) + + # after reloading submit is disabled... + self.assertFalse(submit.is_enabled()) + + # ... until student answers MCQs again + self.click_choice(mcq, "Maybe not") + self.click_choice(rating, "2") + self.assertTrue(submit.is_enabled()) + def test_given_perfect_score_in_past_loads_current_result(self): mentoring = self.load_scenario("feedback_persistence.xml") answer, mcq, mrq, rating = self._get_controls(mentoring) diff --git a/problem_builder/tests/integration/test_titles.py b/problem_builder/tests/integration/test_titles.py index 0d3fe297..34fbae1b 100644 --- a/problem_builder/tests/integration/test_titles.py +++ b/problem_builder/tests/integration/test_titles.py @@ -74,7 +74,7 @@ class StepTitlesTest(SeleniumXBlockTest): mcq_template = """ Gaius Baltar @@ -90,7 +90,7 @@ class StepTitlesTest(SeleniumXBlockTest): mrq_template = """ Lots of choices @@ -103,7 +103,7 @@ class StepTitlesTest(SeleniumXBlockTest): rating_template = """ More than 5 stars diff --git a/problem_builder/tests/unit/test_problem_builder.py b/problem_builder/tests/unit/test_problem_builder.py index 84d8d167..a0dd077e 100644 --- a/problem_builder/tests/unit/test_problem_builder.py +++ b/problem_builder/tests/unit/test_problem_builder.py @@ -1,9 +1,15 @@ -import unittest import ddt +import unittest + from mock import MagicMock, Mock, patch +from random import random + from xblock.field_data import DictFieldData + from problem_builder.mcq import MCQBlock -from problem_builder.mentoring import MentoringBlock, MentoringMessageBlock, _default_theme_config +from problem_builder.mentoring import ( + MentoringBlock, MentoringMessageBlock, _default_theme_config, _default_options_config +) @ddt.ddt @@ -64,45 +70,79 @@ def test_does_not_crash_when_get_child_is_broken(self): @ddt.ddt -class TestMentoringBlockTheming(unittest.TestCase): +class TestMentoringBlockSettings(unittest.TestCase): + + DEFAULT_SETTINGS = { + 'get_theme': _default_theme_config, + 'get_options': _default_options_config, + } + + SETTINGS_KEYS = { + 'get_theme': MentoringBlock.theme_key, + 'get_options': MentoringBlock.options_key, + } + def setUp(self): self.service_mock = Mock() self.runtime_mock = Mock() self.runtime_mock.service = Mock(return_value=self.service_mock) self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) - def test_theme_uses_default_theme_if_settings_service_is_not_available(self): - self.runtime_mock.service = Mock(return_value=None) - self.assertEqual(self.block.get_theme(), _default_theme_config) + def test_settings_method_returns_default_if_settings_service_is_not_available(self): + for settings_method, default_config in self.DEFAULT_SETTINGS.items(): + self.runtime_mock.service = Mock(return_value=None) + self.assertEqual(getattr(self.block, settings_method)(), default_config) - def test_theme_uses_default_theme_if_no_theme_is_set(self): - self.service_mock.get_settings_bucket = Mock(return_value=None) - self.assertEqual(self.block.get_theme(), _default_theme_config) - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) + def test_settings_method_returns_default_if_settings_not_customized(self): + for settings_method, default_config in self.DEFAULT_SETTINGS.items(): + self.service_mock.get_settings_bucket = Mock(return_value=None) + self.assertEqual(getattr(self.block, settings_method)(), default_config) + self.service_mock.get_settings_bucket.assert_called_once_with(self.block) @ddt.data(123, object()) - def test_theme_raises_if_theme_object_is_not_iterable(self, theme_config): - self.service_mock.get_settings_bucket = Mock(return_value=theme_config) - with self.assertRaises(TypeError): - self.block.get_theme() - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) + def test_settings_method_raises_if_settings_not_iterable(self, config): + for settings_method in self.DEFAULT_SETTINGS: + self.service_mock.get_settings_bucket = Mock(return_value=config) + with self.assertRaises(TypeError): + getattr(self.block, settings_method)() + self.service_mock.get_settings_bucket.assert_called_once_with(self.block) @ddt.data( {}, {'mass': 123}, {'spin': {}}, {'parity': "1"} ) - def test_theme_uses_default_theme_if_no_mentoring_theme_is_set_up(self, theme_config): - self.service_mock.get_settings_bucket = Mock(return_value=theme_config) - self.assertEqual(self.block.get_theme(), _default_theme_config) - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) + def test_settings_method_returns_default_if_target_setting_not_customized(self, config): + for settings_method, default_config in self.DEFAULT_SETTINGS.items(): + self.service_mock.get_settings_bucket = Mock(return_value=config) + self.assertEqual(getattr(self.block, settings_method)(), default_config) + self.service_mock.get_settings_bucket.assert_called_once_with(self.block) @ddt.data( - {MentoringBlock.theme_key: 123}, - {MentoringBlock.theme_key: [1, 2, 3]}, - {MentoringBlock.theme_key: {'package': 'qwerty', 'locations': ['something_else.css']}}, + { + MentoringBlock.theme_key: 123, + MentoringBlock.options_key: 123, + }, + { + MentoringBlock.theme_key: [1, 2, 3], + MentoringBlock.options_key: [1, 2, 3], + }, + { + MentoringBlock.theme_key: {'package': 'qwerty', 'locations': ['something_else.css']}, + MentoringBlock.options_key: {'pb_mcq_hide_previous_answer': False}, + }, ) - def test_theme_correctly_returns_configured_theme(self, theme_config): - self.service_mock.get_settings_bucket = Mock(return_value=theme_config) - self.assertEqual(self.block.get_theme(), theme_config[MentoringBlock.theme_key]) + def test_settings_method_correctly_returns_customized_settings(self, config): + for settings_method, settings_key in self.SETTINGS_KEYS.items(): + self.service_mock.get_settings_bucket = Mock(return_value=config) + self.assertEqual(getattr(self.block, settings_method)(), config[settings_key]) + + +@ddt.ddt +class TestMentoringBlockTheming(unittest.TestCase): + def setUp(self): + self.service_mock = Mock() + self.runtime_mock = Mock() + self.runtime_mock.service = Mock(return_value=self.service_mock) + self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) def test_theme_files_are_loaded_from_correct_package(self): fragment = MagicMock() @@ -131,16 +171,41 @@ def test_theme_files_are_added_to_fragment(self, package_name, locations): self.assertEqual(patched_load_unicode.call_count, len(locations)) def test_student_view_calls_include_theme_files(self): + self.service_mock.get_settings_bucket = Mock(return_value={}) with patch.object(self.block, 'include_theme_files') as patched_include_theme_files: fragment = self.block.student_view({}) patched_include_theme_files.assert_called_with(fragment) def test_author_preview_view_calls_include_theme_files(self): + self.service_mock.get_settings_bucket = Mock(return_value={}) with patch.object(self.block, 'include_theme_files') as patched_include_theme_files: fragment = self.block.author_preview_view({}) patched_include_theme_files.assert_called_with(fragment) +@ddt.ddt +class TestMentoringBlockOptions(unittest.TestCase): + def setUp(self): + self.service_mock = Mock() + self.runtime_mock = Mock() + self.runtime_mock.service = Mock(return_value=self.service_mock) + self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) + + def test_get_option(self): + random_key, random_value = random(), random() + with patch.object(self.block, 'get_options') as patched_get_options: + patched_get_options.return_value = {random_key: random_value} + option = self.block.get_option(random_key) + patched_get_options.assert_called_once_with() + self.assertEqual(option, random_value) + + def test_student_view_calls_get_option(self): + self.service_mock.get_settings_bucket = Mock(return_value={}) + with patch.object(self.block, 'get_option') as patched_get_option: + self.block.student_view({}) + patched_get_option.assert_called_with('pb_mcq_hide_previous_answer') + + class TestMentoringBlockJumpToIds(unittest.TestCase): def setUp(self): self.service_mock = Mock() From b17c0eef7d0eb75a47c3ffc0f3fc1d4d9d1eac92 Mon Sep 17 00:00:00 2001 From: Tim Krones Date: Mon, 29 Feb 2016 14:54:18 +0100 Subject: [PATCH 4/4] Address review comments. - Refactor: Take advantage of mixins from xblock-utils accessing xblock settings. --- problem_builder/mentoring.py | 45 ++----- .../tests/integration/test_mentoring.py | 1 - .../tests/unit/test_problem_builder.py | 117 ++++-------------- 3 files changed, 35 insertions(+), 128 deletions(-) diff --git a/problem_builder/mentoring.py b/problem_builder/mentoring.py index 943287a9..8330c6b8 100644 --- a/problem_builder/mentoring.py +++ b/problem_builder/mentoring.py @@ -42,6 +42,7 @@ from xblockutils.helpers import child_isinstance from xblockutils.resources import ResourceLoader +from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin from xblockutils.studio_editable import ( NestedXBlockSpec, StudioEditableXBlockMixin, StudioContainerXBlockMixin, StudioContainerWithNestedXBlocksMixin, ) @@ -84,7 +85,8 @@ def _(text): @XBlock.needs("i18n") @XBlock.wants('settings') class BaseMentoringBlock( - XBlock, XBlockWithTranslationServiceMixin, StudioEditableXBlockMixin, MessageParentMixin + XBlock, XBlockWithTranslationServiceMixin, XBlockWithSettingsMixin, + StudioEditableXBlockMixin, ThemableXBlockMixin, MessageParentMixin, ): """ An XBlock that defines functionality shared by mentoring blocks. @@ -161,45 +163,22 @@ def get_content_titles(self): return [self.display_name] return [] - def get_settings(self, settings_key, default): - """ - Get settings identified by `settings_key` from settings service. - - Fall back on `default` if settings service is unavailable - or settings have not been customized. - """ - settings_service = self.runtime.service(self, "settings") - if settings_service: - xblock_settings = settings_service.get_settings_bucket(self) - if xblock_settings and settings_key in xblock_settings: - return xblock_settings[settings_key] - return default - - def get_theme(self): - """ - Get theme settings for this block from settings service. - - Fall back on default (LMS) theme if settings service is not available, - or theme has not been customized. - """ - return self.get_settings(self.theme_key, _default_theme_config) - - def include_theme_files(self, fragment): - theme = self.get_theme() - theme_package, theme_files = theme['package'], theme['locations'] - for theme_file in theme_files: - fragment.add_css(ResourceLoader(theme_package).load_unicode(theme_file)) - def get_options(self): """ Get options settings for this block from settings service. - Fall back on default options if settings service is not available - or options have not been customized. + Fall back on default options if xblock settings have not been customized at all + or no customizations for options available. """ - return self.get_settings(self.options_key, _default_options_config) + xblock_settings = self.get_xblock_settings(_default_options_config) + if xblock_settings and self.options_key in xblock_settings: + return xblock_settings[self.options_key] + return _default_options_config def get_option(self, option): + """ + Get value of a specific instance-wide `option`. + """ return self.get_options()[option] @XBlock.json_handler diff --git a/problem_builder/tests/integration/test_mentoring.py b/problem_builder/tests/integration/test_mentoring.py index 7a937373..1ec843a1 100644 --- a/problem_builder/tests/integration/test_mentoring.py +++ b/problem_builder/tests/integration/test_mentoring.py @@ -267,7 +267,6 @@ def test_does_not_persist_mcq_feedback_on_page_reload_if_disabled(self): submit = mentoring.find_element_by_css_selector('.submit input.input-main') self._feedback_customized_checks(answer, mcq, mrq, rating, messages) - # after reloading submit is disabled... self.assertFalse(submit.is_enabled()) diff --git a/problem_builder/tests/unit/test_problem_builder.py b/problem_builder/tests/unit/test_problem_builder.py index a0dd077e..663a7309 100644 --- a/problem_builder/tests/unit/test_problem_builder.py +++ b/problem_builder/tests/unit/test_problem_builder.py @@ -69,74 +69,6 @@ def test_does_not_crash_when_get_child_is_broken(self): self.assertIn('Unable to load child component', fragment.content) -@ddt.ddt -class TestMentoringBlockSettings(unittest.TestCase): - - DEFAULT_SETTINGS = { - 'get_theme': _default_theme_config, - 'get_options': _default_options_config, - } - - SETTINGS_KEYS = { - 'get_theme': MentoringBlock.theme_key, - 'get_options': MentoringBlock.options_key, - } - - def setUp(self): - self.service_mock = Mock() - self.runtime_mock = Mock() - self.runtime_mock.service = Mock(return_value=self.service_mock) - self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) - - def test_settings_method_returns_default_if_settings_service_is_not_available(self): - for settings_method, default_config in self.DEFAULT_SETTINGS.items(): - self.runtime_mock.service = Mock(return_value=None) - self.assertEqual(getattr(self.block, settings_method)(), default_config) - - def test_settings_method_returns_default_if_settings_not_customized(self): - for settings_method, default_config in self.DEFAULT_SETTINGS.items(): - self.service_mock.get_settings_bucket = Mock(return_value=None) - self.assertEqual(getattr(self.block, settings_method)(), default_config) - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) - - @ddt.data(123, object()) - def test_settings_method_raises_if_settings_not_iterable(self, config): - for settings_method in self.DEFAULT_SETTINGS: - self.service_mock.get_settings_bucket = Mock(return_value=config) - with self.assertRaises(TypeError): - getattr(self.block, settings_method)() - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) - - @ddt.data( - {}, {'mass': 123}, {'spin': {}}, {'parity': "1"} - ) - def test_settings_method_returns_default_if_target_setting_not_customized(self, config): - for settings_method, default_config in self.DEFAULT_SETTINGS.items(): - self.service_mock.get_settings_bucket = Mock(return_value=config) - self.assertEqual(getattr(self.block, settings_method)(), default_config) - self.service_mock.get_settings_bucket.assert_called_once_with(self.block) - - @ddt.data( - { - MentoringBlock.theme_key: 123, - MentoringBlock.options_key: 123, - }, - { - MentoringBlock.theme_key: [1, 2, 3], - MentoringBlock.options_key: [1, 2, 3], - }, - { - MentoringBlock.theme_key: {'package': 'qwerty', 'locations': ['something_else.css']}, - MentoringBlock.options_key: {'pb_mcq_hide_previous_answer': False}, - }, - ) - def test_settings_method_correctly_returns_customized_settings(self, config): - for settings_method, settings_key in self.SETTINGS_KEYS.items(): - self.service_mock.get_settings_bucket = Mock(return_value=config) - self.assertEqual(getattr(self.block, settings_method)(), config[settings_key]) - - -@ddt.ddt class TestMentoringBlockTheming(unittest.TestCase): def setUp(self): self.service_mock = Mock() @@ -144,32 +76,6 @@ def setUp(self): self.runtime_mock.service = Mock(return_value=self.service_mock) self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) - def test_theme_files_are_loaded_from_correct_package(self): - fragment = MagicMock() - package_name = 'some_package' - theme_config = {MentoringBlock.theme_key: {'package': package_name, 'locations': ['lms.css']}} - self.service_mock.get_settings_bucket = Mock(return_value=theme_config) - with patch("problem_builder.mentoring.ResourceLoader") as patched_resource_loader: - self.block.include_theme_files(fragment) - patched_resource_loader.assert_called_with(package_name) - - @ddt.data( - ('problem_builder', ['public/themes/lms.css']), - ('problem_builder', ['public/themes/lms.css', 'public/themes/lms.part2.css']), - ('my_app.my_rules', ['typography.css', 'icons.css']), - ) - @ddt.unpack - def test_theme_files_are_added_to_fragment(self, package_name, locations): - fragment = MagicMock() - theme_config = {MentoringBlock.theme_key: {'package': package_name, 'locations': locations}} - self.service_mock.get_settings_bucket = Mock(return_value=theme_config) - with patch("problem_builder.mentoring.ResourceLoader.load_unicode") as patched_load_unicode: - self.block.include_theme_files(fragment) - for location in locations: - patched_load_unicode.assert_any_call(location) - - self.assertEqual(patched_load_unicode.call_count, len(locations)) - def test_student_view_calls_include_theme_files(self): self.service_mock.get_settings_bucket = Mock(return_value={}) with patch.object(self.block, 'include_theme_files') as patched_include_theme_files: @@ -191,6 +97,29 @@ def setUp(self): self.runtime_mock.service = Mock(return_value=self.service_mock) self.block = MentoringBlock(self.runtime_mock, DictFieldData({}), Mock()) + def test_get_options_returns_default_if_xblock_settings_not_customized(self): + self.block.get_xblock_settings = Mock(return_value=None) + self.assertEqual(self.block.get_options(), _default_options_config) + self.block.get_xblock_settings.assert_called_once_with(_default_options_config) + + @ddt.data( + {}, {'mass': 123}, {'spin': {}}, {'parity': "1"} + ) + def test_get_options_returns_default_if_options_not_customized(self, xblock_settings): + self.block.get_xblock_settings = Mock(return_value=xblock_settings) + self.assertEqual(self.block.get_options(), _default_options_config) + self.block.get_xblock_settings.assert_called_once_with(_default_options_config) + + @ddt.data( + {MentoringBlock.options_key: 123}, + {MentoringBlock.options_key: [1, 2, 3]}, + {MentoringBlock.options_key: {'pb_mcq_hide_previous_answer': False}}, + ) + def test_get_options_correctly_returns_customized_options(self, xblock_settings): + self.block.get_xblock_settings = Mock(return_value=xblock_settings) + self.assertEqual(self.block.get_options(), xblock_settings[MentoringBlock.options_key]) + self.block.get_xblock_settings.assert_called_once_with(_default_options_config) + def test_get_option(self): random_key, random_value = random(), random() with patch.object(self.block, 'get_options') as patched_get_options: