From 48f4feaa11b651905fd07ba60ffc2b7dfc26c4af Mon Sep 17 00:00:00 2001 From: "E. Kolpakov" Date: Tue, 30 Aug 2016 14:49:52 +0300 Subject: [PATCH] Dropping item into the same zone no longer causes error popup to be shown --- drag_and_drop_v2/drag_and_drop_v2.py | 22 ++++++------ drag_and_drop_v2/public/js/drag_and_drop.js | 9 ++--- drag_and_drop_v2/utils.py | 3 ++ tests/integration/test_base.py | 6 +++- tests/integration/test_interaction.py | 17 ++++++---- .../test_interaction_assessment.py | 34 ++++++++++++++++--- tests/unit/test_basics.py | 12 +++---- 7 files changed, 70 insertions(+), 33 deletions(-) diff --git a/drag_and_drop_v2/drag_and_drop_v2.py b/drag_and_drop_v2/drag_and_drop_v2.py index 41b148b03..fe6407c84 100644 --- a/drag_and_drop_v2/drag_and_drop_v2.py +++ b/drag_and_drop_v2/drag_and_drop_v2.py @@ -17,7 +17,7 @@ from xblockutils.resources import ResourceLoader from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin -from .utils import _, DummyTranslationService, FeedbackMessage, FeedbackMessages, ItemStats, StateMigration +from .utils import _, DummyTranslationService, FeedbackMessage, FeedbackMessages, ItemStats, StateMigration, Constants from .default_data import DEFAULT_DATA @@ -35,8 +35,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): """ XBlock that implements a friendly Drag-and-Drop problem """ - STANDARD_MODE = "standard" - ASSESSMENT_MODE = "assessment" SOLUTION_CORRECT = "correct" SOLUTION_PARTIAL = "partial" @@ -71,10 +69,10 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ), scope=Scope.settings, values=[ - {"display_name": _("Standard"), "value": STANDARD_MODE}, - {"display_name": _("Assessment"), "value": ASSESSMENT_MODE}, + {"display_name": _("Standard"), "value": Constants.STANDARD_MODE}, + {"display_name": _("Assessment"), "value": Constants.ASSESSMENT_MODE}, ], - default=STANDARD_MODE + default=Constants.STANDARD_MODE ) max_attempts = Integer( @@ -372,9 +370,9 @@ def drop_item(self, item_attempt, suffix=''): """ self._validate_drop_item(item_attempt) - if self.mode == self.ASSESSMENT_MODE: + if self.mode == Constants.ASSESSMENT_MODE: return self._drop_item_assessment(item_attempt) - elif self.mode == self.STANDARD_MODE: + elif self.mode == Constants.STANDARD_MODE: return self._drop_item_standard(item_attempt) else: raise JsonHandlerError( @@ -478,7 +476,7 @@ def _validate_do_attempt(self): """ Validates if `do_attempt` handler should be executed """ - if self.mode != self.ASSESSMENT_MODE: + if self.mode != Constants.ASSESSMENT_MODE: raise JsonHandlerError( 400, self.i18n_service.gettext("do_attempt handler should only be called for assessment mode") @@ -496,7 +494,7 @@ def _get_feedback(self): answer_correctness = self._answer_correctness() is_correct = answer_correctness == self.SOLUTION_CORRECT - if self.mode == self.STANDARD_MODE or not self.attempts: + if self.mode == Constants.STANDARD_MODE or not self.attempts: feedback_key = 'finish' if is_correct else 'start' return [FeedbackMessage(self.data['feedback'][feedback_key], None)], set() @@ -698,11 +696,11 @@ def _get_user_state(self): # In assessment mode, if item is placed correctly and than the page is refreshed, "correct" # will spill to the frontend, making item "disabled", thus allowing students to obtain answer by trial # and error + refreshing the page. In order to avoid that, we remove "correct" from an item here - if self.mode == self.ASSESSMENT_MODE: + if self.mode == Constants.ASSESSMENT_MODE: del item["correct"] overall_feedback_msgs, __ = self._get_feedback() - if self.mode == self.STANDARD_MODE: + if self.mode == Constants.STANDARD_MODE: is_finished = self._is_answer_correct() else: is_finished = not self.attempts_remain 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 d6fb47699..7b763cf06 100644 --- a/drag_and_drop_v2/public/js/drag_and_drop.js +++ b/drag_and_drop_v2/public/js/drag_and_drop.js @@ -750,7 +750,7 @@ function DragAndDropBlock(runtime, element, configuration) { var zone = String($zone.data('uid')); var zone_align = $zone.data('zone_align'); - var items_in_zone_count = countItemsInZone(zone); + var items_in_zone_count = countItemsInZone(zone, [item_id.toString()]); if (configuration.max_items_per_zone && configuration.max_items_per_zone <= items_in_zone_count) { state.last_action_correct = false; state.feedback = gettext("You cannot add any more items to this zone."); @@ -770,9 +770,10 @@ function DragAndDropBlock(runtime, element, configuration) { }, 0); }; - var countItemsInZone = function(zone) { - return Object.keys(state.items).filter(function(key) { - return state.items[key].zone === zone; + var countItemsInZone = function(zone, exclude_ids) { + var ids_to_exclude = exclude_ids ? exclude_ids : []; + return Object.keys(state.items).filter(function(item_id) { + return state.items[item_id].zone === zone && $.inArray(item_id, ids_to_exclude) === -1; }).length; }; diff --git a/drag_and_drop_v2/utils.py b/drag_and_drop_v2/utils.py index 6ff7a2721..17c899c69 100644 --- a/drag_and_drop_v2/utils.py +++ b/drag_and_drop_v2/utils.py @@ -91,6 +91,9 @@ class Constants(object): ALLOWED_ZONE_ALIGNMENTS = ['left', 'right', 'center'] DEFAULT_ZONE_ALIGNMENT = 'center' + STANDARD_MODE = "standard" + ASSESSMENT_MODE = "assessment" + class StateMigration(object): """ diff --git a/tests/integration/test_base.py b/tests/integration/test_base.py index 65b440b93..65812b488 100644 --- a/tests/integration/test_base.py +++ b/tests/integration/test_base.py @@ -14,6 +14,8 @@ from xblockutils.base_test import SeleniumBaseTest +from drag_and_drop_v2.utils import Constants + from drag_and_drop_v2.default_data import ( DEFAULT_DATA, START_FEEDBACK, FINISH_FEEDBACK, TOP_ZONE_ID, TOP_ZONE_TITLE, MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE, @@ -49,7 +51,7 @@ class BaseIntegrationTest(SeleniumBaseTest): @classmethod def _make_scenario_xml( cls, display_name="Test DnDv2", show_title=True, problem_text="Question", completed=False, - show_problem_header=True, max_items_per_zone=0, data=None + show_problem_header=True, max_items_per_zone=0, data=None, mode=Constants.STANDARD_MODE ): if not data: data = json.dumps(DEFAULT_DATA) @@ -63,6 +65,7 @@ def _make_scenario_xml( weight='1' completed='{completed}' max_items_per_zone='{max_items_per_zone}' + mode='{mode}' data='{data}' /> @@ -73,6 +76,7 @@ def _make_scenario_xml( show_problem_header=show_problem_header, completed=completed, max_items_per_zone=max_items_per_zone, + mode=mode, data=escape(data, cls._additional_escapes) ) diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py index 4e5f2a9f8..5c0623b58 100644 --- a/tests/integration/test_interaction.py +++ b/tests/integration/test_interaction.py @@ -509,6 +509,8 @@ class TestMaxItemsPerZone(InteractionTestBase, BaseIntegrationTest): PAGE_TITLE = 'Drag and Drop v2' PAGE_ID = 'drag_and_drop_v2' + assessment_mode = False + def _get_scenario_xml(self): scenario_data = loader.load_unicode("data/test_zone_align.json") return self._make_scenario_xml(data=scenario_data, max_items_per_zone=2) @@ -522,12 +524,15 @@ def test_item_returned_to_bank(self): self.place_item(1, zone_id) # precondition check - max items placed into zone - self.assert_placed_item(0, zone_id) - self.assert_placed_item(1, zone_id) + self.assert_placed_item(0, zone_id, assessment_mode=self.assessment_mode) + self.assert_placed_item(1, zone_id, assessment_mode=self.assessment_mode) self.place_item(2, zone_id) self.assert_reverted_item(2) + feedback_popup = self._get_popup() + self.assertTrue(feedback_popup.is_displayed()) + feedback_popup_content = self._get_popup_content() self.assertEqual( feedback_popup_content.get_attribute('innerHTML'), @@ -540,8 +545,8 @@ def test_item_returned_to_bank_after_refresh(self): self.place_item(7, zone_id) # precondition check - max items placed into zone - self.assert_placed_item(6, zone_id) - self.assert_placed_item(7, zone_id) + self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) + self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) self.place_item(8, zone_id) @@ -549,6 +554,6 @@ def test_item_returned_to_bank_after_refresh(self): self._page = self.go_to_page(self.PAGE_TITLE) # refresh the page - self.assert_placed_item(6, zone_id) - self.assert_placed_item(7, zone_id) + self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) + self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) self.assert_reverted_item(8) diff --git a/tests/integration/test_interaction_assessment.py b/tests/integration/test_interaction_assessment.py index 093c7745c..efa50fecf 100644 --- a/tests/integration/test_interaction_assessment.py +++ b/tests/integration/test_interaction_assessment.py @@ -13,9 +13,9 @@ TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID, TOP_ZONE_TITLE, START_FEEDBACK, FINISH_FEEDBACK ) -from drag_and_drop_v2.utils import FeedbackMessages +from drag_and_drop_v2.utils import FeedbackMessages, Constants from .test_base import BaseIntegrationTest -from .test_interaction import InteractionTestBase, DefaultDataTestMixin, ParameterizedTestsMixin +from .test_interaction import InteractionTestBase, DefaultDataTestMixin, ParameterizedTestsMixin, TestMaxItemsPerZone # Globals ########################################################### @@ -33,8 +33,8 @@ class DefaultAssessmentDataTestMixin(DefaultDataTestMixin): def _get_scenario_xml(self): # pylint: disable=no-self-use return """ - - """.format(max_attempts=self.MAX_ATTEMPTS) + + """.format(mode=Constants.ASSESSMENT_MODE, max_attempts=self.MAX_ATTEMPTS) class AssessmentTestMixin(object): @@ -218,3 +218,29 @@ def test_grade(self): published_grade = next((event[0][2] for event in events if event[0][1] == 'grade')) expected_grade = {'max_value': 1, 'value': (1.0 / 5.0)} self.assertEqual(published_grade, expected_grade) + + +class TestMaxItemsPerZoneAssessment(TestMaxItemsPerZone): + assessment_mode = True + + def _get_scenario_xml(self): + scenario_data = loader.load_unicode("data/test_zone_align.json") + return self._make_scenario_xml(data=scenario_data, max_items_per_zone=2, mode=Constants.ASSESSMENT_MODE) + + def test_drop_item_to_same_zone_does_not_show_popup(self): + zone_id = "Zone Left Align" + self.place_item(6, zone_id) + self.place_item(7, zone_id) + + popup = self._get_popup() + + # precondition check - max items placed into zone + self.assert_placed_item(6, zone_id, assessment_mode=self.assessment_mode) + self.assert_placed_item(7, zone_id, assessment_mode=self.assessment_mode) + + self.place_item(6, zone_id, Keys.RETURN) + self.assertFalse(popup.is_displayed()) + + self.place_item(7, zone_id, Keys.RETURN) + self.assertFalse(popup.is_displayed()) + diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py index 8259831b4..864054c86 100644 --- a/tests/unit/test_basics.py +++ b/tests/unit/test_basics.py @@ -1,7 +1,7 @@ import ddt import unittest -from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock +from drag_and_drop_v2.utils import Constants from drag_and_drop_v2.default_data import ( TARGET_IMG_DESCRIPTION, TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID, START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA @@ -23,7 +23,7 @@ def _make_submission(modify_submission=None): submission = { 'display_name': "Test Drag & Drop", - 'mode': DragAndDropBlock.STANDARD_MODE, + 'mode': Constants.STANDARD_MODE, 'max_attempts': 1, 'show_title': False, 'problem_text': "Problem Drag & Drop", @@ -56,7 +56,7 @@ def test_get_configuration(self): zones = config.pop("zones") items = config.pop("items") self.assertEqual(config, { - "mode": DragAndDropBlock.STANDARD_MODE, + "mode": Constants.STANDARD_MODE, "max_attempts": None, "display_zone_borders": False, "display_zone_labels": False, @@ -142,7 +142,7 @@ def test_studio_submit(self): self.assertEqual(res, {'result': 'success'}) self.assertEqual(self.block.show_title, False) - self.assertEqual(self.block.mode, DragAndDropBlock.STANDARD_MODE) + self.assertEqual(self.block.mode, Constants.STANDARD_MODE) self.assertEqual(self.block.max_attempts, 1) self.assertEqual(self.block.display_name, "Test Drag & Drop") self.assertEqual(self.block.question_text, "Problem Drag & Drop") @@ -156,7 +156,7 @@ def test_studio_submit(self): def test_studio_submit_assessment(self): def modify_submission(submission): submission.update({ - 'mode': DragAndDropBlock.ASSESSMENT_MODE, + 'mode': Constants.ASSESSMENT_MODE, 'max_items_per_zone': 4, 'show_problem_header': True, 'show_title': True, @@ -170,7 +170,7 @@ def modify_submission(submission): self.assertEqual(res, {'result': 'success'}) self.assertEqual(self.block.show_title, True) - self.assertEqual(self.block.mode, DragAndDropBlock.ASSESSMENT_MODE) + self.assertEqual(self.block.mode, Constants.ASSESSMENT_MODE) self.assertEqual(self.block.max_attempts, 12) self.assertEqual(self.block.display_name, "Test Drag & Drop") self.assertEqual(self.block.question_text, "Problem Drag & Drop")