diff --git a/README.md b/README.md index 334674061..f27193431 100644 --- a/README.md +++ b/README.md @@ -8,29 +8,30 @@ The editor is fully guided. Features include: * custom target image * free target zone positioning and sizing -* custom size items +* custom zone labels +* custom text and background colors for items * image items * decoy items that don't have a zone * feedback popups for both correct and incorrect attempts * introductory and final feedback -It supports progressive grading and keeps progress across +The XBlock supports progressive grading and keeps progress across refreshes. All checking and record keeping is done on the server side. -The screenshot shows the Drag and Drop XBlock rendered inside the edX -LMS before starting before the user starts solving the problem: +The following screenshot shows the Drag and Drop XBlock rendered +inside the edX LMS before the user starts solving the problem: ![Student view start](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/student-view-start.png) This screenshot shows the XBlock after the student successfully -completed the drag and drop problem: +completed the Drag and Drop problem: ![Student view finish](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/student-view-finish.png) Installation ------------ -Install the requirements into the python virtual environment of your +Install the requirements into the Python virtual environment of your `edx-platform` installation by running the following command from the root folder: @@ -41,12 +42,12 @@ $ pip install -e . Enabling in Studio ------------------ -You can enable the Drag and Drop XBlock in studio through the advanced -settings. +You can enable the Drag and Drop XBlock in Studio through the Advanced +Settings. 1. From the main page of a specific course, navigate to `Settings -> Advanced Settings` from the top menu. -2. Check for the `advanced_modules` policy key, and add +2. Check for the `Advanced Module List` policy key, and add `"drag-and-drop-v2"` to the policy value list. 3. Click the "Save changes" button. @@ -54,13 +55,13 @@ Usage ----- The Drag and Drop XBlock features an interactive editor. Add the Drag -and Drop component to a lesson, then click the 'Edit' button. +and Drop component to a lesson, then click the `EDIT` button. ![Edit view](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view.png) In the first step, you can set some basic properties of the component, -such as the title, question text that rendered above the background -image, the introduction feedback (shown initially) and the final +such as the title, the question text to render above the background +image, the introductory feedback (shown initially) and the final feedback (shown after the student successfully completes the drag and drop problem). @@ -69,21 +70,31 @@ drop problem). In the next step, you set the background image URL and define the properties of the drop zones. The properties include the title/text rendered in the drop zone, the zone's dimensions and position -coordinates. You can define an arbitrary number of drop zones as long -as their titles are unique. +coordinates. In this step you can also specify whether you would like +zone labels to be shown to students or not. It is possible to define +an arbitrary number of drop zones as long as their titles are unique. ![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view-items.png) In the final step, you define the drag items. A drag item can contain -either text or an image. You can define the success and error feedback -texts. The feedback text is displayed in a popup after the student -drops the item into a zone - the success feedback is shown if the item -is dropped into the correct zone, while the error feedback is shown -when dropping the item into a wrong drop zone. +either text or an image. You can define custom success and error feedback +for each item. The feedback text is displayed in a popup after the student +drops the item on a zone - the success feedback is shown if the item +is dropped on the correct zone, while the error feedback is shown +when dropping the item on an incorrect drop zone. + +Additionally, items can have a numerical value (and an optional error +margin) associated with them. When a student drops an item that has a +numerical value on the correct zone, an input field for entering a +value is shown next to the item. The value that the student submits is +checked against the expected value for the item. If you also specify a +margin, the value entered by the student will be considered correct if +it does not differ from the expected value by more than that margin +(and incorrect otherwise). ![Zone dropdown](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/edit-view-zone-dropdown.png) -The zone that the item belongs is selected from a dropdown that +The zone that an item belongs to is selected from a dropdown that includes all drop zones defined in the previous step and a `none` option that can be used for "decoy" items - items that don't belong to any zone. diff --git a/drag_and_drop_v2/default_data.py b/drag_and_drop_v2/default_data.py index ed1e99412..3f27be847 100644 --- a/drag_and_drop_v2/default_data.py +++ b/drag_and_drop_v2/default_data.py @@ -1,11 +1,33 @@ from .utils import _ +TARGET_IMG_DESCRIPTION = _( + "An isosceles triangle with three layers of similar height. " + "It is shown upright, so the widest layer is located at the bottom, " + "and the narrowest layer is located at the top." +) + +TOP_ZONE_TITLE = _("The Top Zone") +MIDDLE_ZONE_TITLE = _("The Middle Zone") +BOTTOM_ZONE_TITLE = _("The Bottom Zone") + +TOP_ZONE_DESCRIPTION = _("Use this zone to associate an item with the top layer of the triangle.") +MIDDLE_ZONE_DESCRIPTION = _("Use this zone to associate an item with the middle layer of the triangle.") +BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom layer of the triangle.") + +ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.") +ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.") + +START_FEEDBACK = _("Drag the items onto the image above.") +FINISH_FEEDBACK = _("Good work! You have completed this drag and drop exercise.") + DEFAULT_DATA = { + "targetImgDescription": TARGET_IMG_DESCRIPTION, "zones": [ { "index": 1, "id": "zone-1", - "title": _("Zone 1"), + "title": TOP_ZONE_TITLE, + "description": TOP_ZONE_DESCRIPTION, "x": 160, "y": 30, "width": 196, @@ -14,47 +36,68 @@ { "index": 2, "id": "zone-2", - "title": _("Zone 2"), + "title": MIDDLE_ZONE_TITLE, + "description": MIDDLE_ZONE_DESCRIPTION, "x": 86, "y": 210, "width": 340, - "height": 140, + "height": 138, + }, + { + "index": 3, + "id": "zone-3", + "title": BOTTOM_ZONE_TITLE, + "description": BOTTOM_ZONE_DESCRIPTION, + "x": 15, + "y": 350, + "width": 485, + "height": 135, } ], "items": [ { - "displayName": "1", + "displayName": _("Goes to the top"), "feedback": { - "incorrect": _("No, 1 does not belong here"), - "correct": _("Yes, it's a 1") + "incorrect": ITEM_INCORRECT_FEEDBACK, + "correct": ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE) }, - "zone": "Zone 1", + "zone": TOP_ZONE_TITLE, "imageURL": "", "id": 0, }, { - "displayName": "2", + "displayName": _("Goes to the middle"), "feedback": { - "incorrect": _("No, 2 does not belong here"), - "correct": _("Yes, it's a 2") + "incorrect": ITEM_INCORRECT_FEEDBACK, + "correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE) }, - "zone": "Zone 2", + "zone": MIDDLE_ZONE_TITLE, "imageURL": "", "id": 1, }, { - "displayName": "X", + "displayName": _("Goes to the bottom"), "feedback": { - "incorrect": _("You silly, there are no zones for X"), + "incorrect": ITEM_INCORRECT_FEEDBACK, + "correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE) + }, + "zone": BOTTOM_ZONE_TITLE, + "imageURL": "", + "id": 2, + }, + { + "displayName": _("I don't belong anywhere"), + "feedback": { + "incorrect": _("You silly, there are no zones for this one."), "correct": "" }, "zone": "none", "imageURL": "", - "id": 2, + "id": 3, }, ], "feedback": { - "start": _("Drag the items onto the image above."), - "finish": _("Good work! You have completed this drag and drop exercise.") + "start": START_FEEDBACK, + "finish": FINISH_FEEDBACK, }, } diff --git a/drag_and_drop_v2/public/css/drag_and_drop_edit.css b/drag_and_drop_v2/public/css/drag_and_drop_edit.css index a9bbbfe98..ad0ca2bca 100644 --- a/drag_and_drop_v2/public/css/drag_and_drop_edit.css +++ b/drag_and_drop_v2/public/css/drag_and_drop_edit.css @@ -203,7 +203,8 @@ .xblock--drag-and-drop--editor .items-form .item-numerical-value, .xblock--drag-and-drop--editor .items-form .item-numerical-margin { - width: 60px; + margin: 0 1%; + width: 50%; } .xblock--drag-and-drop--editor .items-form textarea { diff --git a/drag_and_drop_v2/templates/html/drag_and_drop_edit.html b/drag_and_drop_v2/templates/html/drag_and_drop_edit.html index 04c16100e..d674fb867 100644 --- a/drag_and_drop_v2/templates/html/drag_and_drop_edit.html +++ b/drag_and_drop_v2/templates/html/drag_and_drop_edit.html @@ -20,7 +20,7 @@

{% trans "Question title" %}

{% trans "Maximum score" %}

- +

{% trans "Question text" %}

@@ -30,7 +30,7 @@

{% trans "Question text" %}

{% trans "Show \"Question\" heading" %} -

{% trans "Introduction Feedback" %}

+

{% trans "Introductory Feedback" %}

{% trans "Final Feedback" %}

@@ -48,7 +48,10 @@

{% trans "Zones" %}

{% trans "Background URL" %}

- +

{% trans "Background description" %}

diff --git a/drag_and_drop_v2/templates/html/js_templates.html b/drag_and_drop_v2/templates/html/js_templates.html index d7a38133a..0a1b31572 100644 --- a/drag_and_drop_v2/templates/html/js_templates.html +++ b/drag_and_drop_v2/templates/html/js_templates.html @@ -113,12 +113,20 @@ value="{{ height }}" />
- - + {{i18n "Optional numerical value (if you set this, students will be prompted for this value after dropping this item)"}} + + - - +
+ +
diff --git a/tests/integration/test_interaction.py b/tests/integration/test_interaction.py index 1fe7577f4..d3f174654 100644 --- a/tests/integration/test_interaction.py +++ b/tests/integration/test_interaction.py @@ -1,5 +1,6 @@ from selenium.webdriver import ActionChains +from drag_and_drop_v2.default_data import START_FEEDBACK, FINISH_FEEDBACK from .test_base import BaseIntegrationTest from ..utils import load_resource @@ -161,8 +162,8 @@ class BasicInteractionTest(InteractionTestBase): all_zones = ['Zone 1', 'Zone 2'] feedback = { - "intro": "Drag the items onto the image above.", - "final": "Good work! You have completed this drag and drop exercise." + "intro": START_FEEDBACK, + "final": FINISH_FEEDBACK, } def _get_scenario_xml(self): # pylint: disable=no-self-use diff --git a/tests/integration/test_render.py b/tests/integration/test_render.py index b3151a860..1e7fea901 100644 --- a/tests/integration/test_render.py +++ b/tests/integration/test_render.py @@ -1,6 +1,7 @@ from ddt import ddt, unpack, data from selenium.common.exceptions import NoSuchElementException +from drag_and_drop_v2.default_data import START_FEEDBACK from ..utils import load_resource from .test_base import BaseIntegrationTest @@ -184,7 +185,7 @@ def test_feedback(self): feedback = self._get_feedback() feedback_message = self._get_feedback_message() self.assertEqual(feedback.get_attribute('aria-live'), 'polite') - self.assertEqual(feedback_message.text, "Drag the items onto the image above.") + self.assertEqual(feedback_message.text, START_FEEDBACK) def test_background_image(self): self.load_scenario() diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py index f7ec9887f..73343c37d 100644 --- a/tests/unit/test_basics.py +++ b/tests/unit/test_basics.py @@ -1,11 +1,9 @@ import unittest -from ..utils import ( - DEFAULT_START_FEEDBACK, - DEFAULT_FINISH_FEEDBACK, - make_block, - TestCaseMixin, +from drag_and_drop_v2.default_data import ( + TARGET_IMG_DESCRIPTION, START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA ) +from ..utils import make_block, TestCaseMixin class BasicTests(TestCaseMixin, unittest.TestCase): @@ -36,36 +34,18 @@ def test_get_configuration(self): "question_text": "", "show_question_header": True, "target_img_expanded_url": '/expanded/url/to/drag_and_drop_v2/public/img/triangle.png', - "target_img_description": "", + "target_img_description": TARGET_IMG_DESCRIPTION, "item_background_color": None, "item_text_color": None, - "initial_feedback": DEFAULT_START_FEEDBACK, + "initial_feedback": START_FEEDBACK, }) - self.assertEqual(zones, [ - { - "index": 1, - "title": "Zone 1", - "id": "zone-1", - "x": 160, - "y": 30, - "width": 196, - "height": 178, - }, - { - "index": 2, - "title": "Zone 2", - "id": "zone-2", - "x": 86, - "y": 210, - "width": 340, - "height": 140, - } - ]) + self.assertEqual(zones, DEFAULT_DATA["zones"]) # Items should contain no answer data: self.assertEqual(items, [ - {"id": 0, "displayName": "1", "imageURL": "", "inputOptions": False}, - {"id": 1, "displayName": "2", "imageURL": "", "inputOptions": False}, - {"id": 2, "displayName": "X", "imageURL": "", "inputOptions": False}, + {"id": 0, "displayName": "Goes to the top", "imageURL": "", "inputOptions": False}, + {"id": 1, "displayName": "Goes to the middle", "imageURL": "", "inputOptions": False}, + {"id": 2, "displayName": "Goes to the bottom", "imageURL": "", "inputOptions": False}, + {"id": 3, "displayName": "I don't belong anywhere", "imageURL": "", "inputOptions": False}, ]) def test_ajax_solve_and_reset(self): @@ -77,14 +57,16 @@ def assert_user_state_empty(): self.assertEqual(self.call_handler("get_user_state"), { 'items': {}, 'finished': False, - 'overall_feedback': DEFAULT_START_FEEDBACK, + 'overall_feedback': START_FEEDBACK, }) assert_user_state_empty() - # Drag both items into the correct spot: - data = {"val": 0, "zone": "Zone 1", "x_percent": "33%", "y_percent": "11%"} + # Drag three items into the correct spot: + data = {"val": 0, "zone": "The Top Zone", "x_percent": "33%", "y_percent": "11%"} + self.call_handler('do_attempt', data) + data = {"val": 1, "zone": "The Middle Zone", "x_percent": "67%", "y_percent": "80%"} self.call_handler('do_attempt', data) - data = {"val": 1, "zone": "Zone 2", "x_percent": "67%", "y_percent": "80%"} + data = {"val": 2, "zone": "The Bottom Zone", "x_percent": "99%", "y_percent": "95%"} self.call_handler('do_attempt', data) # Check the result: @@ -92,14 +74,16 @@ def assert_user_state_empty(): self.assertEqual(self.block.item_state, { '0': {'x_percent': '33%', 'y_percent': '11%'}, '1': {'x_percent': '67%', 'y_percent': '80%'}, + '2': {'x_percent': '99%', 'y_percent': '95%'}, }) self.assertEqual(self.call_handler('get_user_state'), { 'items': { '0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True}, '1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True}, + '2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True}, }, 'finished': True, - 'overall_feedback': DEFAULT_FINISH_FEEDBACK, + 'overall_feedback': FINISH_FEEDBACK, }) # Reset to initial conditions diff --git a/tests/utils.py b/tests/utils.py index 8ebc10834..4f75b6574 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -10,9 +10,6 @@ import drag_and_drop_v2 -DEFAULT_START_FEEDBACK = "Drag the items onto the image above." -DEFAULT_FINISH_FEEDBACK = "Good work! You have completed this drag and drop exercise." - def make_request(data, method='POST'): """ Make a webob JSON Request """