diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py
index 08f35cbea..a2bcf5ccb 100644
--- a/drag_and_drop_v2/drag_and_drop_v2.py
+++ b/drag_and_drop_v2/drag_and_drop_v2.py
@@ -190,6 +190,7 @@ def items_without_answers():
return items
return {
+ "mode": self.mode,
"zones": self._get_zones(),
# SDK doesn't supply url_name.
"url_name": getattr(self, 'url_name', ''),
@@ -337,12 +338,18 @@ def do_attempt(self, attempt, suffix=''):
'is_correct': is_correct,
})
- return {
- 'correct': is_correct,
- 'finished': self._is_finished(),
- 'overall_feedback': overall_feedback,
- 'feedback': feedback
- }
+ if self.mode == self.ASSESSMENT_MODE:
+ # In assessment mode we don't send any feedback on drop.
+ result = {}
+ else:
+ result = {
+ 'correct': is_correct,
+ 'finished': self._is_finished(),
+ 'overall_feedback': overall_feedback,
+ 'feedback': feedback
+ }
+
+ return result
@XBlock.json_handler
def reset(self, data, suffix=''):
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 43ad31e54..85ad5f371 100644
--- a/drag_and_drop_v2/public/js/drag_and_drop.js
+++ b/drag_and_drop_v2/public/js/drag_and_drop.js
@@ -1,4 +1,4 @@
-function DragNDropTemplates(url_name) {
+function DragAndDropTemplates(configuration) {
"use strict";
var h = virtualDom.h;
// Set up a mock for gettext if it isn't available in the client runtime:
@@ -107,13 +107,22 @@ function DragNDropTemplates(url_name) {
var item_content = h('div', { innerHTML: item_content_html, className: "item-content" });
if (item.is_placed) {
// Insert information about zone in which this item has been placed
- var item_description_id = url_name + '-item-' + item.value + '-description';
+ var item_description_id = configuration.url_name + '-item-' + item.value + '-description';
item_content.properties.attributes = { 'aria-describedby': item_description_id };
var zone_title = (zone.title || "Unknown Zone"); // This "Unknown" text should never be seen, so does not need i18n
+ var description_content;
+ if (configuration.mode === DragAndDropBlock.ASSESSMENT_MODE) {
+ // In assessment mode placed items will "stick" even when not in correct zone.
+ description_content = gettext('Placed in: {zone_title}').replace('{zone_title}', zone_title);
+ } else {
+ // In standard mode item is immediately returned back to the bank if dropped on a wrong zone,
+ // so all placed items are always in the correct zone.
+ description_content = gettext('Correctly placed in: {zone_title}').replace('{zone_title}', zone_title);
+ }
var item_description = h(
'div',
{ id: item_description_id, className: 'sr' },
- gettext('Correctly placed in: ') + zone_title
+ description_content
);
children.splice(1, 0, item_description);
}
@@ -283,14 +292,17 @@ function DragNDropTemplates(url_name) {
);
};
- DragAndDropBlock.renderView = mainTemplate;
-
+ return mainTemplate;
}
function DragAndDropBlock(runtime, element, configuration) {
"use strict";
- DragNDropTemplates(configuration.url_name);
+ DragAndDropBlock.STANDARD_MODE = 'standard';
+ DragAndDropBlock.ASSESSMENT_MODE = 'assessment';
+
+ var renderView = DragAndDropTemplates(configuration);
+
// Set up a mock for gettext if it isn't available in the client runtime:
if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; }
@@ -747,17 +759,21 @@ function DragAndDropBlock(runtime, element, configuration) {
$.post(url, JSON.stringify(data), 'json')
.done(function(data){
- state.last_action_correct = data.correct;
- if (data.correct) {
- state.items[item_id].correct = true;
- state.items[item_id].submitting_location = false;
- } else {
- delete state.items[item_id];
- }
- state.feedback = data.feedback;
- if (data.finished) {
- state.finished = true;
- state.overall_feedback = data.overall_feedback;
+ state.items[item_id].submitting_location = false;
+ // In standard mode we immediately return item to the bank if dropped on wrong zone.
+ // In assessment mode we leave it in the chosen zone until explicit answer submission.
+ if (configuration.mode === DragAndDropBlock.STANDARD_MODE) {
+ state.last_action_correct = data.correct;
+ if (data.correct) {
+ state.items[item_id].correct = true;
+ } else {
+ delete state.items[item_id];
+ }
+ state.feedback = data.feedback;
+ if (data.finished) {
+ state.finished = true;
+ state.overall_feedback = data.overall_feedback;
+ }
}
applyState();
})
@@ -866,7 +882,7 @@ function DragAndDropBlock(runtime, element, configuration) {
display_reset_button: Object.keys(state.items).length > 0,
};
- return DragAndDropBlock.renderView(context);
+ return renderView(context);
};
/**
diff --git a/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po b/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
index 9b9d624c1..f34ec739d 100644
--- a/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
+++ b/drag_and_drop_v2/translations/en/LC_MESSAGES/text.po
@@ -408,6 +408,10 @@ msgstr ""
msgid "ok"
msgstr ""
+#: public/js/drag_and_drop.js
+msgid "Placed in: "
+msgstr ""
+
#: public/js/drag_and_drop.js
msgid "Correctly placed in: "
msgstr ""
diff --git a/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po b/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
index ad3a2b7ac..51b2e56dc 100644
--- a/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
+++ b/drag_and_drop_v2/translations/eo/LC_MESSAGES/text.po
@@ -493,6 +493,10 @@ msgstr "Çänçél Ⱡ'σяєм ιρѕυ#"
msgid "ok"
msgstr "ök Ⱡ'σя#"
+#: public/js/drag_and_drop.js
+msgid "Placed in: "
+msgstr "Pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
+
#: public/js/drag_and_drop.js
msgid "Correctly placed in: "
msgstr "Çörréçtlý pläçéd ïn: Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py
index cb72a2dd5..a91b91fce 100644
--- a/tests/integration/test_interaction.py
+++ b/tests/integration/test_interaction.py
@@ -6,6 +6,7 @@
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support.ui import WebDriverWait
from workbench.runtime import WorkbenchRuntime
from xblockutils.resources import ResourceLoader
@@ -89,6 +90,19 @@ def _get_zone_position(self, zone_id):
'return $("div[data-uid=\'{zone_id}\']").prevAll(".zone").length'.format(zone_id=zone_id)
)
+ @staticmethod
+ def wait_until_ondrop_xhr_finished(elem):
+ """
+ Waits until the XHR request triggered by dropping the item finishes loading.
+ """
+ wait = WebDriverWait(elem, 2)
+ # While the XHR is in progress, a spinner icon is shown inside the item.
+ # When the spinner disappears, we can assume that the XHR request has finished.
+ wait.until(
+ lambda e: 'fa-spinner' not in e.get_attribute('innerHTML'),
+ u"Spinner should not be in {}".format(elem.get_attribute('innerHTML'))
+ )
+
def place_item(self, item_value, zone_id, action_key=None):
if action_key is None:
self.drag_item_to_zone(item_value, zone_id)
@@ -117,7 +131,7 @@ def move_item_to_zone(self, item_value, zone_id, action_key):
def assert_grabbed_item(self, item):
self.assertEqual(item.get_attribute('aria-grabbed'), 'true')
- def assert_placed_item(self, item_value, zone_title):
+ def assert_placed_item(self, item_value, zone_title, assessment_mode=False):
item = self._get_placed_item_by_value(item_value)
self.wait_until_visible(item)
item_content = item.find_element_by_css_selector('.item-content')
@@ -131,7 +145,10 @@ def assert_placed_item(self, item_value, zone_title):
self.assertEqual(item.get_attribute('data-drag-disabled'), 'true')
self.assertEqual(item_content.get_attribute('aria-describedby'), item_description_id)
self.assertEqual(item_description.get_attribute('id'), item_description_id)
- self.assertEqual(item_description.text, 'Correctly placed in: {}'.format(zone_title))
+ if assessment_mode:
+ self.assertEqual(item_description.text, 'Placed in: {}'.format(zone_title))
+ else:
+ self.assertEqual(item_description.text, 'Correctly placed in: {}'.format(zone_title))
def assert_reverted_item(self, item_value):
item = self._get_item_by_value(item_value)
@@ -152,16 +169,28 @@ def assert_reverted_item(self, item_value):
else:
self.fail('Reverted item should not have .sr description.')
- def assert_decoy_items(self, items_map):
+ def place_decoy_items(self, items_map, action_key):
+ decoy_items = self._get_items_without_zone(items_map)
+ # Place decoy items into first available zone.
+ zone_id, zone_title = self.all_zones[0]
+ for definition in decoy_items.values():
+ self.place_item(definition.item_id, zone_id, action_key)
+ self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True)
+
+ def assert_decoy_items(self, items_map, assessment_mode=False):
decoy_items = self._get_items_without_zone(items_map)
for item_key in decoy_items:
item = self._get_item_by_value(item_key)
-
- self.assertEqual(item.get_attribute('class'), 'option fade')
self.assertEqual(item.get_attribute('aria-grabbed'), 'false')
self.assertEqual(item.get_attribute('data-drag-disabled'), 'true')
-
- def parameterized_item_positive_feedback_on_good_move(self, items_map, scroll_down=100, action_key=None):
+ if assessment_mode:
+ self.assertEqual(item.get_attribute('class'), 'option')
+ else:
+ self.assertEqual(item.get_attribute('class'), 'option fade')
+
+ def parameterized_item_positive_feedback_on_good_move(
+ self, items_map, scroll_down=100, action_key=None, assessment_mode=False
+ ):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
@@ -170,11 +199,20 @@ def parameterized_item_positive_feedback_on_good_move(self, items_map, scroll_do
for definition in self._get_items_with_zone(items_map).values():
self.place_item(definition.item_id, definition.zone_ids[0], action_key)
- self.wait_until_html_in(definition.feedback_positive, feedback_popup_content)
- self.assertEqual(popup.get_attribute('class'), 'popup')
- self.assert_placed_item(definition.item_id, definition.zone_title)
+ self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id))
+ self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=assessment_mode)
+ feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
+ if assessment_mode:
+ self.assertEqual(feedback_popup_html, '')
+ self.assertFalse(popup.is_displayed())
+ else:
+ self.assertEqual(feedback_popup_html, definition.feedback_positive)
+ self.assertEqual(popup.get_attribute('class'), 'popup')
+ self.assertTrue(popup.is_displayed())
- def parameterized_item_negative_feedback_on_bad_move(self, items_map, all_zones, scroll_down=100, action_key=None):
+ def parameterized_item_negative_feedback_on_bad_move(
+ self, items_map, all_zones, scroll_down=100, action_key=None, assessment_mode=False
+ ):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
@@ -182,15 +220,31 @@ def parameterized_item_negative_feedback_on_bad_move(self, items_map, all_zones,
self.scroll_down(pixels=scroll_down)
for definition in items_map.values():
- for zone in all_zones:
- if zone in definition.zone_ids:
- continue
- self.place_item(definition.item_id, zone, action_key)
- self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
- self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
- self.assert_reverted_item(definition.item_id)
-
- def parameterized_final_feedback_and_reset(self, items_map, feedback, scroll_down=100, action_key=None):
+ # Get first zone that is not correct for this item.
+ zone_id = None
+ zone_title = None
+ for z_id, z_title in all_zones:
+ if z_id not in definition.zone_ids:
+ zone_id = z_id
+ zone_title = z_title
+ break
+ if zone_id is not None: # Some items may be placed in any zone, ignore those.
+ self.place_item(definition.item_id, zone_id, action_key)
+ if assessment_mode:
+ self.wait_until_ondrop_xhr_finished(self._get_item_by_value(definition.item_id))
+ feedback_popup_html = feedback_popup_content.get_attribute('innerHTML')
+ self.assertEqual(feedback_popup_html, '')
+ self.assertFalse(popup.is_displayed())
+ self.assert_placed_item(definition.item_id, zone_title, assessment_mode=True)
+ else:
+ self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
+ self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
+ self.assertTrue(popup.is_displayed())
+ self.assert_reverted_item(definition.item_id)
+
+ def parameterized_final_feedback_and_reset(
+ self, items_map, feedback, scroll_down=100, action_key=None, assessment_mode=False
+ ):
feedback_message = self._get_feedback_message()
self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check
@@ -206,12 +260,17 @@ def get_locations():
for item_key, definition in items.items():
self.place_item(definition.item_id, definition.zone_ids[0], action_key)
- self.assert_placed_item(definition.item_id, definition.zone_title)
+ self.assert_placed_item(definition.item_id, definition.zone_title, assessment_mode=assessment_mode)
- self.wait_until_html_in(feedback['final'], self._get_feedback_message())
+ if assessment_mode:
+ # In assessment mode we also place decoy items onto the board,
+ # to make sure they are correctly reverted back to the bank on problem reset.
+ self.place_decoy_items(items_map, action_key)
+ else:
+ self.wait_until_html_in(feedback['final'], self._get_feedback_message())
# Check decoy items
- self.assert_decoy_items(items_map)
+ self.assert_decoy_items(items_map, assessment_mode=assessment_mode)
# Scroll "Reset problem" button into view to make sure Selenium can successfully click it
self.scroll_down(pixels=scroll_down+150)
@@ -298,7 +357,11 @@ class DefaultDataTestMixin(object):
4: ItemDefinition(4, [], None, "", ITEM_NO_ZONE_FEEDBACK),
}
- all_zones = [TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID]
+ all_zones = [
+ (TOP_ZONE_ID, TOP_ZONE_TITLE),
+ (MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE),
+ (BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE)
+ ]
feedback = {
"intro": START_FEEDBACK,
@@ -309,21 +372,66 @@ def _get_scenario_xml(self): # pylint: disable=no-self-use
return ""
-class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase):
+class DefaultAssessmentDataTestMixin(DefaultDataTestMixin):
"""
- Testing interactions with Drag and Drop XBlock against default data. If default data changes this will break.
+ Provides a test scenario with default options in assessment mode.
"""
- def test_item_positive_feedback_on_good_move(self):
- self.parameterized_item_positive_feedback_on_good_move(self.items_map)
+ def _get_scenario_xml(self): # pylint: disable=no-self-use
+ return ""
- def test_item_negative_feedback_on_bad_move(self):
- self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones)
- def test_final_feedback_and_reset(self):
- self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
+@ddt
+class StandardInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
+ """
+ Testing interactions with Drag and Drop XBlock against default data.
+ All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
+ If default data changes this will break.
+ """
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_item_positive_feedback_on_good_move(self, action_key):
+ self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=action_key)
- def test_keyboard_help(self):
- self.interact_with_keyboard_help()
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_item_negative_feedback_on_bad_move(self, action_key):
+ self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones, action_key=action_key)
+
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_final_feedback_and_reset(self, action_key):
+ self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key)
+
+ @data(False, True)
+ def test_keyboard_help(self, use_keyboard):
+ self.interact_with_keyboard_help(use_keyboard=use_keyboard)
+
+
+@ddt
+class AssessmentInteractionTest(DefaultAssessmentDataTestMixin, InteractionTestBase, BaseIntegrationTest):
+ """
+ Testing interactions with Drag and Drop XBlock against default data in assessment mode.
+ All interactions are tested using mouse (action_key=None) and four different keyboard action keys.
+ If default data changes this will break.
+ """
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_item_no_feedback_on_good_move(self, action_key):
+ self.parameterized_item_positive_feedback_on_good_move(
+ self.items_map, action_key=action_key, assessment_mode=True
+ )
+
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_item_no_feedback_on_bad_move(self, action_key):
+ self.parameterized_item_negative_feedback_on_bad_move(
+ self.items_map, self.all_zones, action_key=action_key, assessment_mode=True
+ )
+
+ @data(None, Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
+ def test_final_feedback_and_reset(self, action_key):
+ self.parameterized_final_feedback_and_reset(
+ self.items_map, self.feedback, action_key=action_key, assessment_mode=True
+ )
+
+ @data(False, True)
+ def test_keyboard_help(self, use_keyboard):
+ self.interact_with_keyboard_help(use_keyboard=use_keyboard)
class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
@@ -417,32 +525,14 @@ def test_event(self, index, event):
)
-@ddt
-class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
- @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
- def test_item_positive_feedback_on_good_move_with_keyboard(self, action_key):
- self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=action_key)
-
- @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
- def test_item_negative_feedback_on_bad_move_with_keyboard(self, action_key):
- self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones, action_key=action_key)
-
- @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
- def test_final_feedback_and_reset_with_keyboard(self, action_key):
- self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key)
-
- def test_keyboard_help(self):
- self.interact_with_keyboard_help(use_keyboard=True)
-
-
-class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
+class CustomDataInteractionTest(StandardInteractionTest):
items_map = {
0: ItemDefinition(0, ['zone-1'], "Zone 1", "Yes 1", "No 1"),
1: ItemDefinition(1, ['zone-2'], "Zone 2", "Yes 2", "No 2"),
2: ItemDefinition(2, [], None, "", "No Zone for this")
}
- all_zones = ['zone-1', 'zone-2']
+ all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')]
feedback = {
"intro": "Some Intro Feed",
@@ -453,14 +543,14 @@ def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_data.json")
-class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
+class CustomHtmlDataInteractionTest(StandardInteractionTest):
items_map = {
0: ItemDefinition(0, ['zone-1'], 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, ['zone-2'], 'Zone 2', "Yes 2", "No 2"),
2: ItemDefinition(2, [], None, "", "No Zone for X")
}
- all_zones = ['zone-1', 'zone-2']
+ all_zones = [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')]
feedback = {
"intro": "Intro Feed",
@@ -492,8 +582,8 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
}
all_zones = {
- 'block1': ['zone-1', 'zone-2'],
- 'block2': ['zone-51', 'zone-52']
+ 'block1': [('zone-1', 'Zone 1'), ('zone-2', 'Zone 2')],
+ 'block2': [('zone-51', 'Zone 51'), ('zone-52', 'Zone 52')]
}
feedback = {
diff --git a/tests/unit/data/html/config_out.json b/tests/unit/data/html/config_out.json
index 13ea0f1fe..80f40880c 100644
--- a/tests/unit/data/html/config_out.json
+++ b/tests/unit/data/html/config_out.json
@@ -1,5 +1,6 @@
{
"title": "DnDv2 XBlock with HTML instructions",
+ "mode": "standard",
"show_title": false,
"problem_text": "Solve this drag-and-drop problem.",
"show_problem_header": false,
diff --git a/tests/unit/data/old/config_out.json b/tests/unit/data/old/config_out.json
index 3ef89fa3c..d2ef3efd1 100644
--- a/tests/unit/data/old/config_out.json
+++ b/tests/unit/data/old/config_out.json
@@ -1,5 +1,6 @@
{
"title": "Drag and Drop",
+ "mode": "standard",
"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 0afa35d41..6f60f442b 100644
--- a/tests/unit/data/plain/config_out.json
+++ b/tests/unit/data/plain/config_out.json
@@ -1,5 +1,6 @@
{
"title": "DnDv2 XBlock with plain text instructions",
+ "mode": "standard",
"show_title": true,
"problem_text": "Can you solve this drag-and-drop problem?",
"show_problem_header": true,
diff --git a/tests/unit/test_advanced.py b/tests/unit/test_advanced.py
index ef52190ef..4449fa345 100644
--- a/tests/unit/test_advanced.py
+++ b/tests/unit/test_advanced.py
@@ -5,6 +5,8 @@
from xblockutils.resources import ResourceLoader
+from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock
+
from ..utils import make_block, TestCaseMixin
@@ -90,6 +92,14 @@ 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 = []
diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py
index 31a833e64..4191a1bac 100644
--- a/tests/unit/test_basics.py
+++ b/tests/unit/test_basics.py
@@ -30,6 +30,7 @@ def test_get_configuration(self):
zones = config.pop("zones")
items = config.pop("items")
self.assertEqual(config, {
+ "mode": DragAndDropBlock.STANDARD_MODE,
"display_zone_borders": False,
"display_zone_labels": False,
"title": "Drag and Drop",