Commit fb6b6e73 by Tim Krones

Improve default settings for DnDv2 blocks and explain "Optional

numerical value" and "Margin ±" settings for items.
parent 672aa8c0
...@@ -8,29 +8,30 @@ The editor is fully guided. Features include: ...@@ -8,29 +8,30 @@ The editor is fully guided. Features include:
* custom target image * custom target image
* free target zone positioning and sizing * free target zone positioning and sizing
* custom size items * custom zone labels
* custom text and background colors for items
* image items * image items
* decoy items that don't have a zone * decoy items that don't have a zone
* feedback popups for both correct and incorrect attempts * feedback popups for both correct and incorrect attempts
* introductory and final feedback * 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. refreshes. All checking and record keeping is done on the server side.
The screenshot shows the Drag and Drop XBlock rendered inside the edX The following screenshot shows the Drag and Drop XBlock rendered
LMS before starting before the user starts solving the problem: 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) ![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 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) ![Student view finish](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/5ff71f56ba454c66d8f2749bc1d55d5f1df3b792/doc/img/student-view-finish.png)
Installation 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 `edx-platform` installation by running the following command from the
root folder: root folder:
...@@ -41,12 +42,12 @@ $ pip install -e . ...@@ -41,12 +42,12 @@ $ pip install -e .
Enabling in Studio Enabling in Studio
------------------ ------------------
You can enable the Drag and Drop XBlock in studio through the advanced You can enable the Drag and Drop XBlock in Studio through the Advanced
settings. Settings.
1. From the main page of a specific course, navigate to `Settings -> 1. From the main page of a specific course, navigate to `Settings ->
Advanced Settings` from the top menu. 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. `"drag-and-drop-v2"` to the policy value list.
3. Click the "Save changes" button. 3. Click the "Save changes" button.
...@@ -54,13 +55,13 @@ Usage ...@@ -54,13 +55,13 @@ Usage
----- -----
The Drag and Drop XBlock features an interactive editor. Add the Drag 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) ![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, In the first step, you can set some basic properties of the component,
such as the title, question text that rendered above the background such as the title, the question text to render above the background
image, the introduction feedback (shown initially) and the final image, the introductory feedback (shown initially) and the final
feedback (shown after the student successfully completes the drag and feedback (shown after the student successfully completes the drag and
drop problem). drop problem).
...@@ -69,21 +70,31 @@ drop problem). ...@@ -69,21 +70,31 @@ drop problem).
In the next step, you set the background image URL and define the In the next step, you set the background image URL and define the
properties of the drop zones. The properties include the title/text properties of the drop zones. The properties include the title/text
rendered in the drop zone, the zone's dimensions and position rendered in the drop zone, the zone's dimensions and position
coordinates. You can define an arbitrary number of drop zones as long coordinates. In this step you can also specify whether you would like
as their titles are unique. 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) ![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 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 either text or an image. You can define custom success and error feedback
texts. The feedback text is displayed in a popup after the student for each item. 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 drops the item on a zone - the success feedback is shown if the item
is dropped into the correct zone, while the error feedback is shown is dropped on the correct zone, while the error feedback is shown
when dropping the item into a wrong drop zone. 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) ![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` 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 option that can be used for "decoy" items - items that don't belong to
any zone. any zone.
......
from .utils import _ from .utils import _
INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
DEFAULT_DATA = { DEFAULT_DATA = {
"zones": [ "zones": [
{ {
"index": 1, "index": 1,
"id": "zone-1", "id": "zone-1",
"title": _("Zone 1"), "title": _("The Top Zone"),
"x": 160, "x": 160,
"y": 30, "y": 30,
"width": 196, "width": 196,
...@@ -14,43 +17,62 @@ DEFAULT_DATA = { ...@@ -14,43 +17,62 @@ DEFAULT_DATA = {
{ {
"index": 2, "index": 2,
"id": "zone-2", "id": "zone-2",
"title": _("Zone 2"), "title": _("The Middle Zone"),
"x": 86, "x": 86,
"y": 210, "y": 210,
"width": 340, "width": 340,
"height": 140, "height": 138,
},
{
"index": 3,
"id": "zone-3",
"title": _("The Bottom Zone"),
"x": 15,
"y": 350,
"width": 485,
"height": 135,
} }
], ],
"items": [ "items": [
{ {
"displayName": "1", "displayName": "Goes to the top",
"feedback": { "feedback": {
"incorrect": _("No, 1 does not belong here"), "incorrect": INCORRECT_FEEDBACK,
"correct": _("Yes, it's a 1") "correct": CORRECT_FEEDBACK.format(zone="the top")
}, },
"zone": "Zone 1", "zone": "The Top Zone",
"imageURL": "", "imageURL": "",
"id": 0, "id": 0,
}, },
{ {
"displayName": "2", "displayName": "Goes to the middle",
"feedback": { "feedback": {
"incorrect": _("No, 2 does not belong here"), "incorrect": INCORRECT_FEEDBACK,
"correct": _("Yes, it's a 2") "correct": CORRECT_FEEDBACK.format(zone="the middle")
}, },
"zone": "Zone 2", "zone": "The Middle Zone",
"imageURL": "", "imageURL": "",
"id": 1, "id": 1,
}, },
{ {
"displayName": "X", "displayName": "Goes to the bottom",
"feedback": {
"incorrect": INCORRECT_FEEDBACK,
"correct": CORRECT_FEEDBACK.format(zone="the bottom")
},
"zone": "The Bottom Zone",
"imageURL": "",
"id": 2,
},
{
"displayName": "I don't belong anywhere",
"feedback": { "feedback": {
"incorrect": _("You silly, there are no zones for X"), "incorrect": _("You silly, there are no zones for this one."),
"correct": "" "correct": ""
}, },
"zone": "none", "zone": "none",
"imageURL": "", "imageURL": "",
"id": 2, "id": 3,
}, },
], ],
"feedback": { "feedback": {
......
...@@ -144,11 +144,20 @@ class DragAndDropBlock(XBlock): ...@@ -144,11 +144,20 @@ class DragAndDropBlock(XBlock):
item['inputOptions'] = 'inputOptions' in item item['inputOptions'] = 'inputOptions' in item
return items return items
def title_with_points():
"""
Build title using `display_name` and `weight` of this Drag and Drop exercise.
"""
if self.weight == 1:
return "{title} (1 point possible)".format(title=self.display_name)
else:
return "{title} ({max_grade} points possible)".format(title=self.display_name, max_grade=self.weight)
return { return {
"zones": self.data.get('zones', []), "zones": self.data.get('zones', []),
"display_zone_labels": self.data.get('displayLabels', False), "display_zone_labels": self.data.get('displayLabels', False),
"items": items_without_answers(), "items": items_without_answers(),
"title": self.display_name, "title": title_with_points(),
"show_title": self.show_title, "show_title": self.show_title,
"question_text": self.question_text, "question_text": self.question_text,
"show_question_header": self.show_question_header, "show_question_header": self.show_question_header,
......
...@@ -203,7 +203,15 @@ ...@@ -203,7 +203,15 @@
.xblock--drag-and-drop--editor .items-form .item-numerical-value, .xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop--editor .items-form .item-numerical-margin { .xblock--drag-and-drop--editor .items-form .item-numerical-margin {
width: 60px; margin-right: 1%;
}
.xblock--drag-and-drop--editor .items-form .item-numerical-value {
width: 620px;
}
.xblock--drag-and-drop--editor .items-form .item-numerical-margin {
width: 578px;
} }
.xblock--drag-and-drop--editor .items-form textarea { .xblock--drag-and-drop--editor .items-form textarea {
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
</label> </label>
<h3>{% trans "Maximum score" %}</h3> <h3>{% trans "Maximum score" %}</h3>
<input class="weight" value="1" value="{{ self.weight }}"/> <input class="weight" value="1" value="{{ self.weight }}" />
<h3>{% trans "Question text" %}</h3> <h3>{% trans "Question text" %}</h3>
<textarea class="question-text">{{ self.question_text }}</textarea> <textarea class="question-text">{{ self.question_text }}</textarea>
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
{% trans "Show \"Question\" heading" %} {% trans "Show \"Question\" heading" %}
</label> </label>
<h3>{% trans "Introduction Feedback" %}</h3> <h3>{% trans "Introductory Feedback" %}</h3>
<textarea class="intro-feedback">{{ self.data.feedback.start }}</textarea> <textarea class="intro-feedback">{{ self.data.feedback.start }}</textarea>
<h3>{% trans "Final Feedback" %}</h3> <h3>{% trans "Final Feedback" %}</h3>
...@@ -48,7 +48,10 @@ ...@@ -48,7 +48,10 @@
<h3 id="background-url-label"> <h3 id="background-url-label">
{% trans "Background URL" %} {% trans "Background URL" %}
</h3> </h3>
<input type="text" class="url-input" aria-labelledby="background-url-label"> <input type="text"
class="url-input"
aria-labelledby="background-url-label"
placeholder="e.g. http://example.com/background.png or /static/background.png">
<h3 id="background-description-label"> <h3 id="background-description-label">
{% trans "Background description" %} {% trans "Background description" %}
</h3> </h3>
......
...@@ -113,12 +113,20 @@ ...@@ -113,12 +113,20 @@
value="{{ height }}" /> value="{{ height }}" />
</div> </div>
<div class="row"> <div class="row">
<label for="item-{{id}}-numerical-value">{{i18n "Optional numerical value"}}</label> <label for="item-{{id}}-numerical-value">
<input type="text" {{i18n "Optional numerical value (if you set this, students will be prompted for this value after dropping this item)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-value" id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" /> class="item-numerical-value" value="{{ numericalValue }}" />
<label for="item-{{id}}-numerical-margin">{{i18n "Margin ±"}}</label> </div>
<input type="text" <div class="row">
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (to be considered correct, value entered by user must not differ from expected value by more than this)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-margin" id="item-{{id}}-numerical-margin"
class="item-numerical-margin" value="{{ numericalMargin }}" /> class="item-numerical-margin" value="{{ numericalMargin }}" />
</div> </div>
......
...@@ -53,7 +53,8 @@ class TestDragAndDropTitleAndQuestion(BaseIntegrationTest): ...@@ -53,7 +53,8 @@ class TestDragAndDropTitleAndQuestion(BaseIntegrationTest):
page = self.go_to_page(const_page_name) page = self.go_to_page(const_page_name)
if show_title: if show_title:
problem_header = page.find_element_by_css_selector('h2.problem-header') problem_header = page.find_element_by_css_selector('h2.problem-header')
self.assertEqual(self.get_element_html(problem_header), display_name) expected_header = display_name + ' (1 point possible)'
self.assertEqual(self.get_element_html(problem_header), expected_header)
else: else:
with self.assertRaises(NoSuchElementException): with self.assertRaises(NoSuchElementException):
page.find_element_by_css_selector('h2.problem-header') page.find_element_by_css_selector('h2.problem-header')
{ {
"title": "DnDv2 XBlock with HTML instructions", "title": "DnDv2 XBlock with HTML instructions (1 point possible)",
"show_title": false, "show_title": false,
"question_text": "Solve this <strong>drag-and-drop</strong> problem.", "question_text": "Solve this <strong>drag-and-drop</strong> problem.",
"show_question_header": false, "show_question_header": false,
......
{ {
"title": "Drag and Drop", "title": "Drag and Drop (1 point possible)",
"show_title": true, "show_title": true,
"question_text": "", "question_text": "",
"show_question_header": true, "show_question_header": true,
......
{ {
"title": "DnDv2 XBlock with plain text instructions", "title": "DnDv2 XBlock with plain text instructions (1 point possible)",
"show_title": true, "show_title": true,
"question_text": "Can you solve this drag-and-drop problem?", "question_text": "Can you solve this drag-and-drop problem?",
"show_question_header": true, "show_question_header": true,
......
...@@ -31,7 +31,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -31,7 +31,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
items = config.pop("items") items = config.pop("items")
self.assertEqual(config, { self.assertEqual(config, {
"display_zone_labels": False, "display_zone_labels": False,
"title": "Drag and Drop", "title": "Drag and Drop (1 point possible)",
"show_title": True, "show_title": True,
"question_text": "", "question_text": "",
"show_question_header": True, "show_question_header": True,
...@@ -44,8 +44,8 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -44,8 +44,8 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(zones, [ self.assertEqual(zones, [
{ {
"index": 1, "index": 1,
"title": "Zone 1",
"id": "zone-1", "id": "zone-1",
"title": "The Top Zone",
"x": 160, "x": 160,
"y": 30, "y": 30,
"width": 196, "width": 196,
...@@ -53,19 +53,29 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -53,19 +53,29 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
}, },
{ {
"index": 2, "index": 2,
"title": "Zone 2",
"id": "zone-2", "id": "zone-2",
"title": "The Middle Zone",
"x": 86, "x": 86,
"y": 210, "y": 210,
"width": 340, "width": 340,
"height": 140, "height": 138,
},
{
"index": 3,
"id": "zone-3",
"title": "The Bottom Zone",
"x": 15,
"y": 350,
"width": 485,
"height": 135,
} }
]) ])
# Items should contain no answer data: # Items should contain no answer data:
self.assertEqual(items, [ self.assertEqual(items, [
{"id": 0, "displayName": "1", "imageURL": "", "inputOptions": False}, {"id": 0, "displayName": "Goes to the top", "imageURL": "", "inputOptions": False},
{"id": 1, "displayName": "2", "imageURL": "", "inputOptions": False}, {"id": 1, "displayName": "Goes to the middle", "imageURL": "", "inputOptions": False},
{"id": 2, "displayName": "X", "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): def test_ajax_solve_and_reset(self):
...@@ -81,10 +91,12 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -81,10 +91,12 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
}) })
assert_user_state_empty() assert_user_state_empty()
# Drag both items into the correct spot: # Drag three items into the correct spot:
data = {"val": 0, "zone": "Zone 1", "x_percent": "33%", "y_percent": "11%"} 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) 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) self.call_handler('do_attempt', data)
# Check the result: # Check the result:
...@@ -92,11 +104,13 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -92,11 +104,13 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(self.block.item_state, { self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%'}, '0': {'x_percent': '33%', 'y_percent': '11%'},
'1': {'x_percent': '67%', 'y_percent': '80%'}, '1': {'x_percent': '67%', 'y_percent': '80%'},
'2': {'x_percent': '99%', 'y_percent': '95%'},
}) })
self.assertEqual(self.call_handler('get_user_state'), { self.assertEqual(self.call_handler('get_user_state'), {
'items': { 'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True}, '0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True},
'1': {'x_percent': '67%', 'y_percent': '80%', '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, 'finished': True,
'overall_feedback': DEFAULT_FINISH_FEEDBACK, 'overall_feedback': DEFAULT_FINISH_FEEDBACK,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment