diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py index a2bcf5ccb..4c88ed1a7 100644 --- a/drag_and_drop_v2/drag_and_drop_v2.py +++ b/drag_and_drop_v2/drag_and_drop_v2.py @@ -127,6 +127,12 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): default={}, ) + num_attempts = Integer( + help=_("Number of attempts learner used"), + scope=Scope.user_state, + default=0 + ) + completed = Boolean( help=_("Indicates whether a learner has completed the problem at least once"), scope=Scope.user_state, @@ -191,6 +197,7 @@ def items_without_answers(): return { "mode": self.mode, + "max_attempts": self.max_attempts, "zones": self._get_zones(), # SDK doesn't supply url_name. "url_name": getattr(self, 'url_name', ''), @@ -433,6 +440,7 @@ def _get_user_state(self): return { 'items': item_state, 'finished': is_finished, + 'num_attempts': self.num_attempts, 'overall_feedback': self.data['feedback']['finish' if is_finished else 'start'], } diff --git a/drag_and_drop_v2/public/css/drag_and_drop.css b/drag_and_drop_v2/public/css/drag_and_drop.css index ed7217adb..06de5039b 100644 --- a/drag_and_drop_v2/public/css/drag_and_drop.css +++ b/drag_and_drop_v2/public/css/drag_and_drop.css @@ -326,13 +326,174 @@ outline: 2px solid white; } -/*** KEYBOARD HELP ***/ +/*** edX pattern library components ***/ + +/* reset stock edx-platform button styles */ +.xblock--drag-and-drop button, +.xblock--drag-and-drop button:hover, +.xblock--drag-and-drop button.is-hovered, +.xblock--drag-and-drop button:focus, +.xblock--drag-and-drop button.is-focused, +.xblock--drag-and-drop button:active, +.xblock--drag-and-drop button.is-active { + box-shadow: none; + text-shadow: none; + background-image: none; +} + +.xblock--drag-and-drop .btn-default, +.xblock--drag-and-drop .btn-brand { + display: inline-block; + font-weight: normal; + background: #0079bc; + color: #fcfcfc; + -webkit-transition: color 0.125s ease-in-out 0s, border-color 0.125s ease-in-out 0s, background 0.125s ease-in-out 0s, box-shadow 0.125s ease-in-out 0s; + -moz-transition: color 0.125s ease-in-out 0s, border-color 0.125s ease-in-out 0s, background 0.125s ease-in-out 0s, box-shadow 0.125s ease-in-out 0s; + transition: color 0.125s ease-in-out 0s, border-color 0.125s ease-in-out 0s, background 0.125s ease-in-out 0s, box-shadow 0.125s ease-in-out 0s; + border-radius: 3px; + border: 1px solid #0079bc; + padding: 0.625em 1.25em; + font-size: 1em; +} + +.xblock--drag-and-drop .btn-default { + border-color: transparent; + background: transparent; + color: #0074b4 +} + +.xblock--drag-and-drop .btn-brand { + border-color: #0075b4; + background: white; + color: #0075b4; +} + +.xblock--drag-and-drop .btn-small { + padding: 0.625em 0.625em; + font-size: 0.875em; +} + +.xblock--drag-and-drop .btn-default:hover, +.xblock--drag-and-drop .btn-default.is-hovered, +.xblock--drag-and-drop .btn-default:focus, +.xblock--drag-and-drop .btn-default.is-focused { + background-color: transparent; + border: 1px solid #0074b4; + color: #0074b4 +} + +.xblock--drag-and-drop .btn-default:active, +.xblock--drag-and-drop .btn-default.is-pressed, +.xblock--drag-and-drop .btn-default.is-active { + border-color: #0074b4; + color: #0074b4 +} + +.xblock--drag-and-drop .btn-default:disabled, +.xblock--drag-and-drop .btn-default.is-disabled { + color: #6b6969 +} + +.xblock--drag-and-drop .btn-brand { + border-color: #0074b4; + background: #0074b4; + color: #fcfcfc +} + +.xblock--drag-and-drop .btn-brand:hover, +.xblock--drag-and-drop .btn-brand.is-hovered, +.xblock--drag-and-drop .btn-brand:focus, +.xblock--drag-and-drop .btn-brand.is-focused { + border-color: #008bd8; + background-color: #008bd8; + color: #fcfcfc +} + +.xblock--drag-and-drop .btn-brand:active, +.xblock--drag-and-drop .btn-brand.is-pressed, +.xblock--drag-and-drop .btn-brand.is-active { + border-color: #0074b4; + background: #0074b4 +} + +.xblock--drag-and-drop .btn-brand:disabled, +.xblock--drag-and-drop .btn-brand.is-disabled { + border-color: #d2d0d0; + background: #f2f3f3; + color: #676666 +} + + +/*** ACTIONS TOOLBAR ***/ + +.xblock--drag-and-drop .actions-toolbar { + min-height: 3.75em; + width: auto; + position: relative; +} + +.xblock--drag-and-drop .actions-toolbar .action-toolbar-item { + display: inline-block; + margin: 10px 0; +} + +.xblock--drag-and-drop .attempts-used { + margin-left: 0.675em; + font-size: 0.875em; + color: #666; +} + +.xblock--drag-and-drop .actions-toolbar .action-toolbar-item.sidebar-buttons { + text-align: left; + display: block; +} + +@media (min-width: 768px) { + .xblock--drag-and-drop .actions-toolbar .action-toolbar-item.sidebar-buttons { + float: right; + margin: 0; + padding-right: -5px; + } +} + +.xblock--drag-and-drop .sidebar-buttons .sidebar-button-wrapper { + border-right: 1px solid #ddd; + border-collapse: collapse; + padding: 0 5px; + display: inline-block; + height: 100%; +} + +.xblock--drag-and-drop .sidebar-buttons .sidebar-button-wrapper:first-child { + padding-left: 0; +} + +.xblock--drag-and-drop .sidebar-buttons .sidebar-button-wrapper:last-child { + border-right: none; + padding-right: 0; +} + +.xblock--drag-and-drop .sidebar-buttons .btn-brand { + display: inline-block; + padding: 3px 5px; +} .xblock--drag-and-drop .keyboard-help { margin-top: 3px; margin-bottom: 6px; } +.xblock--drag-and-drop .btn-icon { + display: block; +} + +.xblock--drag-and-drop .reset-button { + margin-top: 3px; +} + +/*** ACTIONS TOOLBAR END ***/ + +/*** KEYBOARD HELP ***/ .xblock--drag-and-drop .keyboard-help-dialog { position: fixed; left: 50%; @@ -382,26 +543,7 @@ margin-left: 2%; } -.xblock--drag-and-drop .link-button { - padding: 0; - margin: 0; - cursor: pointer; - color: #2d74b3; - font-weight: normal; - font-size: 12pt; - background: none; - box-shadow: none; - border: none; -} - -.xblock--drag-and-drop .reset-button { - float: right; - margin-top: 3px; -} - -.xblock--drag-and-drop .link-button:focus { - outline: 2px solid #2d74b3; -} +/*** KEYBOARD HELP END ***/ /* Make sure screen-reader content is hidden in the workbench: */ .xblock--drag-and-drop .sr { diff --git a/drag_and_drop_v2/public/js/drag_and_drop.js b/drag_and_drop_v2/public/js/drag_and_drop.js index 18f48cf7b..10d85bf67 100644 --- a/drag_and_drop_v2/public/js/drag_and_drop.js +++ b/drag_and_drop_v2/public/js/drag_and_drop.js @@ -228,51 +228,87 @@ function DragAndDropTemplates(configuration) { var feedbackTemplate = function(ctx) { var feedback_display = ctx.feedback_html ? 'block' : 'none'; - var reset_button_display = ctx.display_reset_button ? 'block' : 'none'; var properties = { attributes: { 'aria-live': 'polite' } }; return ( h('section.feedback', properties, [ - h( - 'button.reset-button.unbutton.link-button', - { style: { display: reset_button_display }, attributes: { tabindex: 0 }, 'aria-live': 'off'}, - gettext('Reset problem') - ), h('h3.title1', { style: { display: feedback_display } }, gettext('Feedback')), h('p.message', { style: { display: feedback_display }, innerHTML: ctx.feedback_html }) ]) ); }; - var keyboardHelpTemplate = function(ctx) { - var dialog_attributes = { role: 'dialog', 'aria-labelledby': 'modal-window-title' }; - var dialog_style = {}; + var keyboardHelpPopupTemplate = function(ctx) { + var labelledby_id = 'modal-window-title-'+configuration.url_name; return ( - h('section.keyboard-help', [ - h('button.keyboard-help-button.unbutton.link-button', { attributes: { tabindex: 0 } }, gettext('Keyboard Help')), - h('div.keyboard-help-dialog', [ - h('div.modal-window-overlay'), - h('div.modal-window', { attributes: dialog_attributes, style: dialog_style }, [ - h('div.modal-header', [ - h('h2.modal-window-title', gettext('Keyboard Help')) - ]), - h('div.modal-content', [ - h('p', gettext('You can complete this problem using only your keyboard.')), - h('ul', [ - h('li', gettext('Use "Tab" and "Shift-Tab" to navigate between items and zones.')), - h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, then navigate to the zone you want to drop it on.')), - h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" to drop the item on the current zone.')), - h('li', gettext('Press "Esc" if you want to cancel the drop operation (for example, to select a different item).')), - ]) - ]), - h('div.modal-actions', [ - h('button.modal-dismiss-button', gettext("OK")) + h('div.keyboard-help-dialog', [ + h('div.modal-window-overlay'), + h('div.modal-window', {attributes: {role: 'dialog', 'aria-labelledby': labelledby_id}}, [ + h('div.modal-header', [ + h('h2.modal-window-title#'+labelledby_id, gettext('Keyboard Help')) + ]), + h('div.modal-content', [ + h('p', gettext('You can complete this problem using only your keyboard.')), + h('ul', [ + h('li', gettext('Use "Tab" and "Shift-Tab" to navigate between items and zones.')), + h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" on an item to select it for dropping, then navigate to the zone you want to drop it on.')), + h('li', gettext('Press "Enter", "Space", "Ctrl-m", or "⌘-m" to drop the item on the current zone.')), + h('li', gettext('Press "Esc" if you want to cancel the drop operation (for example, to select a different item).')), ]) + ]), + h('div.modal-actions', [ + h('button.modal-dismiss-button', gettext("OK")) ]) ]) ]) ); }; + var submitAnswerTemplate = function(ctx) { + var attemptsUsedId = "attempts-used-"+configuration.url_name; + var attemptsUsedDisplay = (ctx.max_attempts && ctx.max_attempts > 0) ? 'inline': 'none'; + var button_enabled = ctx.items.some(function(item) {return item.is_placed;}) && + (ctx.max_attempts === null || ctx.max_attempts > ctx.num_attempts); + + return ( + h("section.action-toolbar-item.submit-answer", {}, [ + h( + "button.btn-brand.submit-answer-button", + {disabled: !button_enabled, attributes: {"aria-describedby": attemptsUsedId}}, + gettext("Submit") + ), + h( + "span.attempts-used#"+attemptsUsedId, {style: {display: attemptsUsedDisplay}}, + gettext("You have used {used} of {total} attempts.") + .replace("{used}", ctx.num_attempts).replace("{total}", ctx.max_attempts) + ) + ]) + ); + }; + + var sidebarButtonTemplate = function(buttonClass, iconClass, buttonText, disabled) { + return ( + h('span.sidebar-button-wrapper', {}, [ + h( + 'button.unbutton.btn-default.btn-small.'+buttonClass, + {disabled: disabled || false, attributes: {tabindex: 0}}, + [ + h("span.btn-icon.fa."+iconClass, {attributes: {"aria-hidden": true}}, []), + buttonText + ] + ) + ]) + ); + }; + + var sidebarTemplate = function(ctx) { + return( + h("section.action-toolbar-item.sidebar-buttons", {}, [ + sidebarButtonTemplate("keyboard-help-button", "fa-question", gettext('Keyboard Help')), + sidebarButtonTemplate("reset-button", "fa-refresh", gettext('Reset'), ctx.disable_reset_button), + ]) + ) + }; + var mainTemplate = function(ctx) { var problemTitle = ctx.show_title ? h('h2.problem-title', {innerHTML: ctx.title_html}) : null; var problemHeader = ctx.show_problem_header ? h('h3.title1', gettext('Problem')) : null; @@ -336,7 +372,11 @@ function DragAndDropTemplates(configuration) { renderCollection(zoneTemplate, ctx.zones, ctx) ]), ]), - keyboardHelpTemplate(ctx), + h("section.actions-toolbar", {}, [ + sidebarTemplate(ctx), + (ctx.show_submit_answer ? submitAnswerTemplate(ctx) : null), + ]), + keyboardHelpPopupTemplate(ctx), feedbackTemplate(ctx), ]) ); @@ -655,6 +695,10 @@ function DragAndDropBlock(runtime, element, configuration) { zones[idx].focus(); }; + var focusFirstDraggable = function() { + $root.find('.item-bank .option').first().focus(); + }; + var placeItem = function($zone, $item) { var item_id; var $anchor; @@ -918,13 +962,11 @@ function DragAndDropBlock(runtime, element, configuration) { type: 'POST', url: runtime.handlerUrl(element, 'reset'), data: '{}', + }).done(function(data) { + state = data; + applyState(); + focusFirstDraggable(); }); - state = { - 'items': [], - 'finished': false, - 'overall_feedback': configuration.initial_feedback, - }; - applyState(); }; var render = function() { @@ -984,8 +1026,12 @@ function DragAndDropBlock(runtime, element, configuration) { bg_image_width: bgImgNaturalWidth, // Not stored in configuration since it's unknown on the server side title_html: configuration.title, show_title: configuration.show_title, + mode: configuration.mode, + max_attempts: configuration.max_attempts, + num_attempts: state.num_attempts, problem_html: configuration.problem_text, show_problem_header: configuration.show_problem_header, + show_submit_answer: configuration.mode == DragAndDropBlock.ASSESSMENT_MODE, target_img_src: configuration.target_img_expanded_url, target_img_description: configuration.target_img_description, display_zone_labels: configuration.display_zone_labels, @@ -997,7 +1043,7 @@ function DragAndDropBlock(runtime, element, configuration) { item_bank_focusable: item_bank_focusable, popup_html: state.feedback || '', feedback_html: $.trim(state.overall_feedback), - display_reset_button: Object.keys(state.items).length > 0, + disable_reset_button: Object.keys(state.items).length == 0, }; return renderView(context); @@ -1035,8 +1081,8 @@ function DragAndDropBlock(runtime, element, configuration) { if (configuration.items[i].id === +item_id) { var size = configuration.items[i].size; // size is an object like '{width: "50px", height: "auto"}' - if (parseInt(size.width ) > 0) { width = parseInt(size.width); } - if (parseInt(size.height) > 0) { height = parseInt(size.height); } + if (parseInt(size.width ) > 0) {width = parseInt(size.width);} + if (parseInt(size.height) > 0) {height = parseInt(size.height);} break; } } diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py index a0be8d4c5..ab71d1e8c 100644 --- a/tests/integration/test_base.py +++ b/tests/integration/test_base.py @@ -74,14 +74,20 @@ def _get_keyboard_help(self): return self._page.find_element_by_css_selector(".keyboard-help") def _get_keyboard_help_button(self): - return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-button") + return self._page.find_element_by_css_selector(".keyboard-help-button") def _get_keyboard_help_dialog(self): - return self._page.find_element_by_css_selector(".keyboard-help .keyboard-help-dialog") + return self._page.find_element_by_css_selector(".keyboard-help-dialog") def _get_reset_button(self): return self._page.find_element_by_css_selector('.reset-button') + def _get_submit_button(self): + return self._page.find_element_by_css_selector('.submit-answer-button') + + def _get_attempts_info(self): + return self._page.find_element_by_css_selector('.attempts-used') + def _get_feedback(self): return self._page.find_element_by_css_selector(".feedback") diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py index 6860a3640..d9de6d5cc 100644 --- a/tests/integration/test_interaction.py +++ b/tests/integration/test_interaction.py @@ -473,8 +473,12 @@ class DefaultAssessmentDataTestMixin(DefaultDataTestMixin): """ Provides a test scenario with default options in assessment mode. """ + MAX_ATTEMPTS = 5 + def _get_scenario_xml(self): # pylint: disable=no-self-use - return "" + return """ + + """.format(max_attempts=self.MAX_ATTEMPTS) @ddt @@ -542,6 +546,22 @@ def test_final_feedback_and_reset(self, action_key): def test_keyboard_help(self, use_keyboard): self.interact_with_keyboard_help(use_keyboard=use_keyboard) + def test_submit_button_shown(self): + first_item_definition = self._get_items_with_zone(self.items_map).values()[0] + + submit_button = self._get_submit_button() + self.assertTrue(submit_button.is_displayed()) + self.assertEqual(submit_button.get_attribute('disabled'), 'true') # no items are placed + + attempts_info = self._get_attempts_info() + expected_text = "You have used {num} of {max} attempts.".format(num=0, max=self.MAX_ATTEMPTS) + self.assertEqual(attempts_info.text, expected_text) + self.assertEqual(attempts_info.is_displayed(), self.MAX_ATTEMPTS > 0) + + self.place_item(first_item_definition.item_id, first_item_definition.zone_ids[0], None) + + self.assertEqual(submit_button.get_attribute('disabled'), None) + class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest): diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index b45e26791..e8537a3fa 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -213,7 +213,6 @@ def test_popup(self): def test_keyboard_help(self): self.load_scenario() - self._get_keyboard_help() keyboard_help_button = self._get_keyboard_help_button() keyboard_help_dialog = self._get_keyboard_help_dialog() dialog_modal_overlay = keyboard_help_dialog.find_element_by_css_selector('.modal-window-overlay') @@ -223,7 +222,7 @@ def test_keyboard_help(self): self.assertFalse(dialog_modal_overlay.is_displayed()) self.assertFalse(dialog_modal.is_displayed()) self.assertEqual(dialog_modal.get_attribute('role'), 'dialog') - self.assertEqual(dialog_modal.get_attribute('aria-labelledby'), 'modal-window-title') + self.assertEqual(dialog_modal.get_attribute('aria-labelledby'), 'modal-window-title-') def test_feedback(self): self.load_scenario() diff --git a/tests/unit/data/assessment/config_out.json b/tests/unit/data/assessment/config_out.json new file mode 100644 index 000000000..1b31e8f25 --- /dev/null +++ b/tests/unit/data/assessment/config_out.json @@ -0,0 +1,62 @@ +{ + "title": "DnDv2 XBlock with plain text instructions", + "mode": "assessment", + "max_attempts": 10, + "show_title": true, + "problem_text": "Can you solve this drag-and-drop problem?", + "show_problem_header": true, + "target_img_expanded_url": "http://placehold.it/800x600", + "target_img_description": "This describes the target image", + "item_background_color": null, + "item_text_color": null, + "initial_feedback": "This is the initial feedback.", + "display_zone_borders": false, + "display_zone_labels": false, + "url_name": "test", + + "zones": [ + { + "title": "Zone 1", + "y": 123, + "x": 234, + "width": 345, + "height": 456, + "uid": "zone-1" + }, + { + "title": "Zone 2", + "y": 20, + "x": 10, + "width": 30, + "height": 40, + "uid": "zone-2" + } + ], + + "items": [ + { + "displayName": "1", + "imageURL": "", + "expandedImageURL": "", + "id": 0 + }, + { + "displayName": "2", + "imageURL": "", + "expandedImageURL": "", + "id": 1 + }, + { + "displayName": "X", + "imageURL": "/static/test_url_expansion", + "expandedImageURL": "/course/test-course/assets/test_url_expansion", + "id": 2 + }, + { + "displayName": "", + "imageURL": "http://placehold.it/200x100", + "expandedImageURL": "http://placehold.it/200x100", + "id": 3 + } + ] +} diff --git a/tests/unit/data/assessment/data.json b/tests/unit/data/assessment/data.json new file mode 100644 index 000000000..0a0de5373 --- /dev/null +++ b/tests/unit/data/assessment/data.json @@ -0,0 +1,75 @@ +{ + "zones": [ + { + "title": "Zone 1", + "y": 123, + "x": 234, + "width": 345, + "height": 456, + "uid": "zone-1" + }, + { + "title": "Zone 2", + "y": 20, + "x": 10, + "width": 30, + "height": 40, + "uid": "zone-2" + } + ], + + + "items": [ + { + "displayName": "1", + "feedback": { + "incorrect": "No 1", + "correct": "Yes 1" + }, + "zone": "zone-1", + "imageURL": "", + "id": 0 + }, + { + "displayName": "2", + "feedback": { + "incorrect": "No 2", + "correct": "Yes 2" + }, + "zone": "zone-2", + "imageURL": "", + "id": 1 + }, + { + "displayName": "X", + "feedback": { + "incorrect": "", + "correct": "" + }, + "zone": "none", + "imageURL": "/static/test_url_expansion", + "id": 2 + }, + { + "displayName": "", + "feedback": { + "incorrect": "", + "correct": "" + }, + "zone": "none", + "imageURL": "http://placehold.it/200x100", + "id": 3 + } + ], + + + "feedback": { + "start": "This is the initial feedback.", + "finish": "This is the final feedback." + }, + + + "targetImg": "http://placehold.it/800x600", + "targetImgDescription": "This describes the target image", + "displayLabels": false +} diff --git a/tests/unit/data/assessment/settings.json b/tests/unit/data/assessment/settings.json new file mode 100644 index 000000000..59b9de1b4 --- /dev/null +++ b/tests/unit/data/assessment/settings.json @@ -0,0 +1,12 @@ +{ + "display_name": "DnDv2 XBlock with plain text instructions", + "mode": "assessment", + "max_attempts": 10, + "show_title": true, + "question_text": "Can you solve this drag-and-drop problem?", + "show_question_header": true, + "weight": 1, + "item_background_color": "", + "item_text_color": "", + "url_name": "test" +} diff --git a/tests/unit/data/html/config_out.json b/tests/unit/data/html/config_out.json index 80f40880c..7e809ea43 100644 --- a/tests/unit/data/html/config_out.json +++ b/tests/unit/data/html/config_out.json @@ -1,6 +1,7 @@ { "title": "DnDv2 XBlock with HTML instructions", "mode": "standard", + "max_attempts": 0, "show_title": false, "problem_text": "Solve this drag-and-drop problem.", "show_problem_header": false, diff --git a/tests/unit/data/html/settings.json b/tests/unit/data/html/settings.json index 1db59684d..dc26eb779 100644 --- a/tests/unit/data/html/settings.json +++ b/tests/unit/data/html/settings.json @@ -1,5 +1,6 @@ { "display_name": "DnDv2 XBlock with HTML instructions", + "max_attempts": 0, "show_title": false, "question_text": "Solve this drag-and-drop problem.", "show_question_header": false, diff --git a/tests/unit/data/old/config_out.json b/tests/unit/data/old/config_out.json index d2ef3efd1..f4c0dd888 100644 --- a/tests/unit/data/old/config_out.json +++ b/tests/unit/data/old/config_out.json @@ -1,6 +1,7 @@ { "title": "Drag and Drop", "mode": "standard", + "max_attempts": null, "show_title": true, "problem_text": "", "show_problem_header": true, diff --git a/tests/unit/data/plain/config_out.json b/tests/unit/data/plain/config_out.json index 6f60f442b..fd116d481 100644 --- a/tests/unit/data/plain/config_out.json +++ b/tests/unit/data/plain/config_out.json @@ -1,6 +1,7 @@ { "title": "DnDv2 XBlock with plain text instructions", "mode": "standard", + "max_attempts": 0, "show_title": true, "problem_text": "Can you solve this drag-and-drop problem?", "show_problem_header": true, diff --git a/tests/unit/data/plain/settings.json b/tests/unit/data/plain/settings.json index fb3a97ad7..27d7c22e5 100644 --- a/tests/unit/data/plain/settings.json +++ b/tests/unit/data/plain/settings.json @@ -1,5 +1,6 @@ { "display_name": "DnDv2 XBlock with plain text instructions", + "max_attempts": 0, "show_title": true, "question_text": "Can you solve this drag-and-drop problem?", "show_question_header": true, diff --git a/tests/unit/test_advanced.py b/tests/unit/test_advanced.py index 4449fa345..3be85e7ea 100644 --- a/tests/unit/test_advanced.py +++ b/tests/unit/test_advanced.py @@ -5,8 +5,6 @@ from xblockutils.resources import ResourceLoader -from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock - from ..utils import make_block, TestCaseMixin @@ -57,8 +55,13 @@ def initial_feedback(cls): return cls.expected_configuration()["initial_feedback"] def test_get_configuration(self): - self.assertEqual(self.expected_configuration(), self.block.get_configuration()) + self.assertEqual(self.block.get_configuration(), self.expected_configuration()) + +class StandardModeFixture(BaseDragAndDropAjaxFixture): + """ + Common tests for drag and drop in standard mode + """ def test_do_attempt_wrong_with_feedback(self): item_id, zone_id = 0, self.ZONE_2 data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"} @@ -92,14 +95,6 @@ def test_do_attempt_correct(self): "feedback": self.FEEDBACK[item_id]["correct"] }) - def test_do_attempt_in_assessment_mode(self): - self.block.mode = DragAndDropBlock.ASSESSMENT_MODE - item_id, zone_id = 0, self.ZONE_1 - data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"} - res = self.call_handler('do_attempt', data) - # In assessment mode, the do_attempt doesn't return any data. - self.assertEqual(res, {}) - def test_grading(self): published_grades = [] @@ -131,6 +126,7 @@ def test_do_attempt_final(self): "0": {"x_percent": "33%", "y_percent": "11%", "correct": True, "zone": self.ZONE_1} }, "finished": False, + "num_attempts": 0, 'overall_feedback': self.initial_feedback(), } self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET")) @@ -154,12 +150,25 @@ def test_do_attempt_final(self): } }, "finished": True, + "num_attempts": 0, 'overall_feedback': self.FINAL_FEEDBACK, } self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET")) -class TestDragAndDropHtmlData(BaseDragAndDropAjaxFixture, unittest.TestCase): +class AssessmentModeFixture(BaseDragAndDropAjaxFixture): + """ + Common tests for drag and drop in assessment mode + """ + def test_do_attempt_in_assessment_mode(self): + item_id, zone_id = 0, self.ZONE_1 + data = {"val": item_id, "zone": zone_id, "x_percent": "33%", "y_percent": "11%"} + res = self.call_handler('do_attempt', data) + # In assessment mode, the do_attempt doesn't return any data. + self.assertEqual(res, {}) + + +class TestDragAndDropHtmlData(StandardModeFixture, unittest.TestCase): FOLDER = "html" ZONE_1 = "Zone 1" @@ -174,7 +183,7 @@ class TestDragAndDropHtmlData(BaseDragAndDropAjaxFixture, unittest.TestCase): FINAL_FEEDBACK = "Final feedback!" -class TestDragAndDropPlainData(BaseDragAndDropAjaxFixture, unittest.TestCase): +class TestDragAndDropPlainData(StandardModeFixture, unittest.TestCase): FOLDER = "plain" ZONE_1 = "zone-1" @@ -198,3 +207,18 @@ class TestOldDataFormat(TestDragAndDropPlainData): ZONE_1 = "Zone 1" ZONE_2 = "Zone 2" + + +class TestDragAndDropAssessmentData(AssessmentModeFixture, unittest.TestCase): + FOLDER = "assessment" + + ZONE_1 = "zone-1" + ZONE_2 = "zone-2" + + FEEDBACK = { + 0: {"correct": "Yes 1", "incorrect": "No 1"}, + 1: {"correct": "Yes 2", "incorrect": "No 2"}, + 2: {"correct": "", "incorrect": ""} + } + + FINAL_FEEDBACK = "This is the final feedback." diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py index 4191a1bac..b7c570980 100644 --- a/tests/unit/test_basics.py +++ b/tests/unit/test_basics.py @@ -31,6 +31,7 @@ def test_get_configuration(self): items = config.pop("items") self.assertEqual(config, { "mode": DragAndDropBlock.STANDARD_MODE, + "max_attempts": None, "display_zone_borders": False, "display_zone_labels": False, "title": "Drag and Drop", @@ -68,6 +69,7 @@ def assert_user_state_empty(): self.assertEqual(self.call_handler("get_user_state"), { 'items': {}, 'finished': False, + "num_attempts": 0, 'overall_feedback': START_FEEDBACK, }) assert_user_state_empty() @@ -98,6 +100,7 @@ def assert_user_state_empty(): '3': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, "zone": MIDDLE_ZONE_ID}, }, 'finished': True, + "num_attempts": 0, 'overall_feedback': FINISH_FEEDBACK, })