Commit 3849de48 by Jesse Shapiro Committed by GitHub

[SOL-1909] Multiple Drop Targets per Item (#83)

Allowing items to have more than one permitted target zone
parent 7fd8c2ed
......@@ -131,17 +131,16 @@ items, as well as the drag items themselves. A drag item can contain
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 learner drops the item on a zone - the success feedback is
shown if the item is dropped on the correct zone, while the error
shown if the item is dropped on a correct zone, while the error
feedback is shown when dropping the item on an incorrect drop zone.
![Zone dropdown](/doc/img/edit-view-zone-dropdown.png)
You can select any number of zones for an item to belong to using
the checkboxes; all zones defined in the previous step are available.
You can leave all of the checkboxes unchecked in order to create a
"decoy" item that doesn't belong to any zone.
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.
You can define an arbitrary number of drag items.
You can define an arbitrary number of drag items, each of which may
be attached to any number of zones.
Demo Course
-----------
......
doc/img/edit-view-items.png

37.4 KB | W: | H:

doc/img/edit-view-items.png

117 KB | W: | H:

doc/img/edit-view-items.png
doc/img/edit-view-items.png
doc/img/edit-view-items.png
doc/img/edit-view-items.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -21,6 +21,7 @@ BOTTOM_ZONE_DESCRIPTION = _("Use this zone to associate an item with the bottom
ITEM_CORRECT_FEEDBACK = _("Correct! This one belongs to {zone}.")
ITEM_INCORRECT_FEEDBACK = _("No, this item does not belong here. Try again.")
ITEM_NO_ZONE_FEEDBACK = _("You silly, there are no zones for this one.")
ITEM_ANY_ZONE_FEEDBACK = _("Of course it goes here! It goes anywhere!")
START_FEEDBACK = _("Drag the items onto the image above.")
FINISH_FEEDBACK = _("Good work! You have completed this drag and drop problem.")
......@@ -63,7 +64,9 @@ DEFAULT_DATA = {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE)
},
"zone": TOP_ZONE_ID,
"zones": [
TOP_ZONE_ID
],
"imageURL": "",
"id": 0,
},
......@@ -73,7 +76,9 @@ DEFAULT_DATA = {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE)
},
"zone": MIDDLE_ZONE_ID,
"zones": [
MIDDLE_ZONE_ID
],
"imageURL": "",
"id": 1,
},
......@@ -83,19 +88,35 @@ DEFAULT_DATA = {
"incorrect": ITEM_INCORRECT_FEEDBACK,
"correct": ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE)
},
"zone": BOTTOM_ZONE_ID,
"zones": [
BOTTOM_ZONE_ID
],
"imageURL": "",
"id": 2,
},
{
"displayName": _("Goes anywhere"),
"feedback": {
"incorrect": "",
"correct": ITEM_ANY_ZONE_FEEDBACK
},
"zones": [
TOP_ZONE_ID,
BOTTOM_ZONE_ID,
MIDDLE_ZONE_ID
],
"imageURL": "",
"id": 3
},
{
"displayName": _("I don't belong anywhere"),
"feedback": {
"incorrect": ITEM_NO_ZONE_FEEDBACK,
"correct": ""
},
"zone": "none",
"zones": [],
"imageURL": "",
"id": 3,
"id": 4,
},
],
"feedback": {
......
......@@ -166,7 +166,11 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
items = copy.deepcopy(self.data.get('items', ''))
for item in items:
del item['feedback']
del item['zone']
# Use item.pop to remove both `item['zone']` and `item['zones']`; we don't have
# a guarantee that either will be present, so we can't use `del`. Legacy instances
# will have `item['zone']`, while current versions will have `item['zones']`.
item.pop('zone', None)
item.pop('zones', None)
# Fall back on "backgroundImage" to be backward-compatible.
image_url = item.get('imageURL') or item.get('backgroundImage')
if image_url:
......@@ -233,6 +237,19 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
# Do a bit of manipulation so we get the appearance of a list of zone options on
# items that still have just a single zone stored
items = self.data.get('items', [])
for item in items:
zones = self._get_item_zones(item['id'])
# Note that we appear to be mutating the state of the XBlock here, but because
# the change won't be committed, we're actually just affecting the data that
# we're going to send to the client, not what's saved in the backing store.
item['zones'] = zones
item.pop('zone', None)
fragment.initialize_js('DragAndDropEditBlock', {
'data': self.data,
'target_img_expanded_url': self.target_img_expanded_url,
......@@ -267,7 +284,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
overall_feedback = None
is_correct = False
if item['zone'] == attempt['zone']: # Student placed item in correct zone
if self._is_attempt_correct(attempt): # Student placed item in a correct zone
is_correct = True
feedback = item['feedback']['correct']
state = {
......@@ -321,6 +338,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
self.item_state = {}
return self._get_user_state()
def _is_attempt_correct(self, attempt):
"""
Check if the item was placed correctly.
"""
correct_zones = self._get_item_zones(attempt['val'])
return attempt['zone'] in correct_zones
def _expand_static_url(self, url):
"""
This is required to make URLs like '/static/dnd-test-image.png' work (note: that is the
......@@ -373,12 +397,19 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
""" Get all user-specific data, and any applicable feedback """
item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
definition = self._get_item_definition(int(item_id))
# If information about zone is missing
# (because problem was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
if item.get('zone') is None:
item['zone'] = definition.get('zone', 'unknown')
valid_zones = self._get_item_zones(int(item_id))
if valid_zones:
# If we get to this point, then the item was placed prior to support for
# multiple correct zones being added. As a result, it can only be correct
# on a single zone, and so we can trust that the item was placed on the
# zone with index 0.
item['zone'] = valid_zones[0]
else:
item['zone'] = 'unknown'
is_finished = self._is_finished()
return {
......@@ -408,6 +439,24 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
return next(i for i in self.data['items'] if i['id'] == item_id)
def _get_item_zones(self, item_id):
"""
Returns a list of the zones that are valid options for the item.
If the item is configured with a list of zones, return that list. If
the item is configured with a single zone, encapsulate that zone's
ID in a list and return the list. If the item is not configured with
any zones, or if it's configured explicitly with no zones, return an
empty list.
"""
item = self._get_item_definition(item_id)
if item.get('zones') is not None:
return item.get('zones')
elif item.get('zone') is not None and item.get('zone') != 'none':
return [item.get('zone')]
else:
return []
def _get_zones(self):
"""
Get drop zone data, defined by the author.
......@@ -432,39 +481,37 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
if zone["uid"] == uid:
return zone
def _get_grade(self):
def _get_item_stats(self):
"""
Returns the student's grade for this block.
Returns a tuple representing the number of correctly-placed items,
and the total number of items that must be placed on the board (non-decoy items).
"""
correct_count = 0
total_count = 0
all_items = self.data['items']
item_state = self._get_item_state()
for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
correct_count += 1
required_items = [str(item['id']) for item in all_items if self._get_item_zones(item['id']) != []]
placed_items = [item for item in required_items if item in item_state]
correct_items = [item for item in placed_items if item_state[item]['correct']]
required_count = len(required_items)
correct_count = len(correct_items)
return correct_count, required_count
return correct_count / float(total_count) * self.weight
def _get_grade(self):
"""
Returns the student's grade for this block.
"""
correct_count, required_count = self._get_item_stats()
return correct_count / float(required_count) * self.weight
def _is_finished(self):
"""
All items are at their correct place and a value has been
submitted for each item that expects a value.
"""
completed_count = 0
total_count = 0
item_state = self._get_item_state()
for item in self.data['items']:
if item['zone'] != 'none':
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
completed_count += 1
return completed_count == total_count
correct_count, required_count = self._get_item_stats()
return correct_count == required_count
@XBlock.json_handler
def publish_event(self, data, suffix=''):
......
......@@ -178,17 +178,22 @@
margin: 15px 0;
}
.xblock--drag-and-drop--editor .items-form label {
.xblock--drag-and-drop--editor .items-form label,
.xblock--drag-and-drop--editor .items-form .h3 {
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form input,
.xblock--drag-and-drop--editor .items-form select {
width: 35%;
}
.xblock--drag-and-drop--editor .items-form .zone-checkbox {
width: initial;
}
.xblock--drag-and-drop--editor .items-form .item-text,
.xblock--drag-and-drop--editor .items-form .item-image-url {
width: 81%;
width: 50%;
margin: 0 1%;
}
......@@ -201,6 +206,10 @@
margin: 0 1%;
}
.xblock--drag-and-drop--editor .items-form .zone-checkbox-label {
text-align: left;
}
.xblock--drag-and-drop--editor .items-form .row {
margin-bottom: 20px;
}
......@@ -211,6 +220,9 @@
padding-left: 1em;
font-size: 80%;
}
.xblock--drag-and-drop-editor .items-form .zone-checkbox-row {
margin-bottom: 0px;
}
/** Buttons **/
.xblock--drag-and-drop--editor .btn {
......@@ -243,8 +255,9 @@
}
.xblock--drag-and-drop--editor .remove-item {
float: right;
display: inline-block;
margin-left: 95px;
margin-right: 16px;
}
.xblock--drag-and-drop--editor .icon {
......
......@@ -31,7 +31,7 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.tpl = {
zoneInput: Handlebars.compile($("#zone-input-tpl", element).html()),
zoneElement: Handlebars.compile($("#zone-element-tpl", element).html()),
zoneDropdown: Handlebars.compile($("#zone-dropdown-tpl", element).html()),
zoneCheckbox: Handlebars.compile($("#zone-checkbox-tpl", element).html()),
itemInput: Handlebars.compile($("#item-input-tpl", element).html()),
};
}
......@@ -357,26 +357,20 @@ function DragAndDropEditBlock(runtime, element, params) {
_fn.build.form.zone.renderZonesPreview();
},
},
createDropdown: function(selectedUID) {
var template = _fn.tpl.zoneDropdown;
var dropdown = [];
createCheckboxes: function(selectedZones) {
var template = _fn.tpl.zoneCheckbox;
var checkboxes = [];
var zoneObjects = _fn.build.form.zone.zoneObjects;
zoneObjects.forEach(function(zoneObj) {
dropdown.push(template({
uid: zoneObj.uid,
checkboxes.push(template({
zoneUid: zoneObj.uid,
title: zoneObj.title,
selected: (zoneObj.uid == selectedUID) ? 'selected' : '',
checked: $.inArray(zoneObj.uid, selectedZones) !== -1 ? 'checked' : '',
}));
});
dropdown.push(template({
uid: "none",
title: window.gettext("None"),
selected: (selectedUID === "none") ? 'selected' : '',
}));
var html = dropdown.join('');
var html = checkboxes.join('');
return new Handlebars.SafeString(html);
},
feedback: function($form) {
......@@ -415,8 +409,7 @@ function DragAndDropEditBlock(runtime, element, params) {
ctx.pixelHeight = itemData.size.height.substr(0, itemData.size.height.length - 2); // Remove 'px'
}
}
ctx.dropdown = _fn.build.form.createDropdown(ctx.zone);
ctx.checkboxes = _fn.build.form.createCheckboxes(ctx.zones);
_fn.build.form.item.count++;
$form.append(tpl(ctx));
......@@ -464,12 +457,15 @@ function DragAndDropEditBlock(runtime, element, params) {
var $el = $(el),
name = $el.find('.item-text').val(),
imageURL = $el.find('.item-image-url').val(),
imageDescription = $el.find('.item-image-description').val();
imageDescription = $el.find('.item-image-description').val(),
selectedZones = $el.find('.zone-checkbox:checked');
if (name.length > 0 || imageURL.length > 0) {
var data = {
displayName: name,
zone: $el.find('.zone-select').val(),
zones: $.map(selectedZones, function(checkbox){
return checkbox.value;
}),
id: i,
feedback: {
correct: $el.find('.success-feedback').val(),
......
......@@ -82,45 +82,61 @@
</div>
</script>
<script id="zone-dropdown-tpl" type="text/html">
<option value="{{ uid }}" {{ selected }}>{{ title }}</option>
<script id="zone-checkbox-tpl" type="text/html">
<div class="zone-checkbox-row">
<label>
<input type="checkbox"
value="{{ zoneUid }}"
class="zone-checkbox"
{{ checked }} />
{{ title }}
</label>
</div>
</script>
<script id="item-input-tpl" type="text/html">
<div class="item">
<div class="row">
<label for="item-{{id}}-text">{{i18n "Text"}}</label>
<input type="text"
id="item-{{id}}-text"
class="item-text"
value="{{ displayName }}" />
<label for="item-{{id}}-zone">{{i18n "Zone"}}</label>
<select id="item-{{id}}-zone"
class="zone-select">{{ dropdown }}</select>
<label class="h3">
{{i18n "Text"}}
<input type="text"
placeholder="{{i18n 'Use text that is clear and descriptive of the item to be placed'}}"
class="item-text"
value="{{ displayName }}" />
</label>
<a href="#" class="remove-item hidden">
<div class="icon remove"></div>
</a>
</div>
<div class="row">
<label for="item-{{id}}-image-url">{{i18n "Image URL (alternative to the text)"}}</label>
<input type="text"
id="item-{{id}}-image-url"
placeholder="{{i18n 'For example, http://example.com/image.png or /static/image.png'}}"
class="item-image-url"
value="{{ imageURL }}" />
<fieldset>
<legend class="h3">
{{ i18n "Zones" }}
</legend>
{{ checkboxes }}
</fieldset>
</div>
<div class="row">
<label class="h3">
{{i18n "Image URL (alternative to the text)"}}
<input type="text"
placeholder="{{i18n 'For example, http://example.com/image.png or /static/image.png'}}"
class="item-image-url"
value="{{ imageURL }}" />
</label>
</div>
<div class="row">
<label for="item-{{id}}-image-description">{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<label class="h3" for="item-{{id}}-image-description">{{i18n "Image description (should provide sufficient information to place the item even if the image did not load)"}}</label>
<textarea id="item-{{id}}-image-description" {{#if imageURL}}required{{/if}}
class="item-image-description">{{ imageDescription }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<label class="h3" for="item-{{id}}-success-feedback">{{i18n "Success Feedback"}}</label>
<textarea id="item-{{id}}-success-feedback"
class="success-feedback">{{ feedback.correct }}</textarea>
</div>
<div class="row">
<label for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<label class="h3" for="item-{{id}}-error-feedback">{{i18n "Error Feedback"}}</label>
<textarea id="item-{{id}}-error-feedback"
class="error-feedback">{{ feedback.incorrect }}</textarea>
</div>
......@@ -128,8 +144,14 @@
<a href="#">{{i18n "Show advanced settings" }}</a>
</div>
<div class="row advanced">
<label for="item-{{id}}-width-percent">{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}</label>
<input type="number" id="item-{{id}}-width-percent" class="item-width" value="{{ singleDecimalFloat widthPercent }}" step="0.1" min="1" max="99" />%
<label>
{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}
<input type="number"
class="item-width"
value="{{ singleDecimalFloat widthPercent }}"
step="0.1"
min="1"
max="99" />%
</div>
</div>
</script>
......@@ -19,6 +19,14 @@ msgid ""
msgstr ""
#: default_data.py
msgid "Of course it goes here! It goes anywhere!"
msgstr ""
#: default_data.py
msgid "Goes anywhere"
msgstr ""
#: default_data.py
msgid "The Top Zone"
msgstr ""
......@@ -248,6 +256,14 @@ msgid ""
"automatic width):"
msgstr ""
#: templates/html/js_templates.html
msgid "Zones"
msgstr ""
#: templates/html/js_templates.html
msgid "Use text that is clear and descriptive of the item to be placed"
msgstr ""
#: templates/html/drag_and_drop.html
msgid "Loading drag and drop problem."
msgstr ""
......
......@@ -104,6 +104,14 @@ msgstr "Göés tö thé ßöttöm Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт α
msgid "I don't belong anywhere"
msgstr "Ì dön't ßélöng änýwhéré Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: default_data.py
msgid "Of course it goes here! It goes anywhere!"
msgstr "Öf çöürsé ït göés héré! Ìt göés änýwhéré! Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя #"
#: default_data.py
msgid "Goes anywhere"
msgstr "Göés änýwhéré Ⱡ'σяєм ιρѕυм ∂σłσя ѕι#"
#: drag_and_drop_v2.py
msgid "Title"
msgstr "Tïtlé Ⱡ'σяєм ιρѕ#"
......@@ -304,6 +312,16 @@ msgstr ""
"Préférréd wïdth äs ä pérçéntägé öf thé ßäçkgröünd ïmägé wïdth (ör ßlänk för "
"äütömätïç wïdth): Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: templates/html/js_templates.html
msgid "Zones"
msgstr "Zönés Ⱡ'σяєм ιρѕ#"
#: templates/html/js_templates.html
msgid "Use text that is clear and descriptive of the item to be placed"
msgstr ""
"Ûsé téxt thät ïs çléär änd désçrïptïvé öf thé ïtém tö ßé pläçéd Ⱡ'σяєм "
"ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: templates/html/drag_and_drop.html
msgid "Loading drag and drop problem."
msgstr "Löädïng dräg änd dröp prößlém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
......
{
"zones": [
{
"width": 200,
"title": "Zone 1",
"height": 100,
"y": "200",
"x": "100",
"uid": "zone-1"
},
{
"width": 200,
"title": "Zone 2",
"height": 100,
"y": 0,
"x": 0,
"uid": "zone-2"
}
],
"items": [
{
"displayName": "1 here",
"feedback": {
"incorrect": "No 1",
"correct": "Yes 1"
},
"zones": [
"zone-1",
"zone-2"
],
"imageURL": "",
"id": 0
},
{
"displayName": "2 here",
"feedback": {
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "zone-2",
"imageURL": "",
"id": 1
},
{
"displayName": "X",
"feedback": {
"incorrect": "No Zone for this",
"correct": ""
},
"zone": "none",
"imageURL": "",
"id": 2
}
],
"feedback": {
"start": "Some Intro Feed",
"finish": "Some Final Feed"
}
}
......@@ -14,7 +14,7 @@ from drag_and_drop_v2.default_data import (
TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID,
TOP_ZONE_TITLE, MIDDLE_ZONE_TITLE, BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK, ITEM_INCORRECT_FEEDBACK, ITEM_NO_ZONE_FEEDBACK,
START_FEEDBACK, FINISH_FEEDBACK
ITEM_ANY_ZONE_FEEDBACK, START_FEEDBACK, FINISH_FEEDBACK
)
from .test_base import BaseIntegrationTest
......@@ -27,10 +27,10 @@ loader = ResourceLoader(__name__)
# Classes ###########################################################
class ItemDefinition(object):
def __init__(self, item_id, zone_id, zone_title, feedback_positive, feedback_negative):
def __init__(self, item_id, zone_ids, zone_title, feedback_positive, feedback_negative):
self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive
self.zone_id = zone_id
self.zone_ids = zone_ids
self.zone_title = zone_title
self.item_id = item_id
......@@ -40,14 +40,14 @@ class InteractionTestBase(object):
def _get_items_with_zone(cls, items_map):
return {
item_key: definition for item_key, definition in items_map.items()
if definition.zone_id is not None
if definition.zone_ids != []
}
@classmethod
def _get_items_without_zone(cls, items_map):
return {
item_key: definition for item_key, definition in items_map.items()
if definition.zone_id is None
if definition.zone_ids == []
}
def setUp(self):
......@@ -169,7 +169,7 @@ class InteractionTestBase(object):
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
self.place_item(definition.item_id, definition.zone_id, action_key)
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)
......@@ -183,7 +183,7 @@ class InteractionTestBase(object):
for definition in items_map.values():
for zone in all_zones:
if zone == definition.zone_id:
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)
......@@ -205,7 +205,7 @@ class InteractionTestBase(object):
self.scroll_down(pixels=scroll_down)
for item_key, definition in items.items():
self.place_item(definition.item_id, definition.zone_id, action_key)
self.place_item(definition.item_id, definition.zone_ids[0], action_key)
self.assert_placed_item(definition.item_id, definition.zone_title)
self.wait_until_html_in(feedback['final'], self._get_feedback_message())
......@@ -280,18 +280,22 @@ class DefaultDataTestMixin(object):
items_map = {
0: ItemDefinition(
0, TOP_ZONE_ID, TOP_ZONE_TITLE,
0, [TOP_ZONE_ID], TOP_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=TOP_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
1: ItemDefinition(
1, MIDDLE_ZONE_ID, MIDDLE_ZONE_TITLE,
1, [MIDDLE_ZONE_ID], MIDDLE_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=MIDDLE_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
2: ItemDefinition(
2, BOTTOM_ZONE_ID, BOTTOM_ZONE_TITLE,
2, [BOTTOM_ZONE_ID], BOTTOM_ZONE_TITLE,
ITEM_CORRECT_FEEDBACK.format(zone=BOTTOM_ZONE_TITLE), ITEM_INCORRECT_FEEDBACK
),
3: ItemDefinition(3, None, None, "", ITEM_NO_ZONE_FEEDBACK),
3: ItemDefinition(
3, [MIDDLE_ZONE_ID, TOP_ZONE_ID, BOTTOM_ZONE_ID], MIDDLE_ZONE_TITLE,
ITEM_ANY_ZONE_FEEDBACK, ITEM_INCORRECT_FEEDBACK
),
4: ItemDefinition(4, [], None, "", ITEM_NO_ZONE_FEEDBACK),
}
all_zones = [TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID]
......@@ -322,6 +326,30 @@ class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase):
self.interact_with_keyboard_help()
class MultipleValidOptionsInteractionTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, ['zone-1', 'zone-2'], ["Zone 1", "Zone 2"], ["Yes 1", "Yes 1"], ["No 1", "No 1"]),
}
def test_multiple_positive_feedback(self):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
reset = self._get_reset_button()
self.scroll_down(pixels=100)
for item in self.items_map.values():
for i, zone in enumerate(item.zone_ids):
self.place_item(item.item_id, zone, None)
self.wait_until_html_in(item.feedback_positive[i], feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup')
self.assert_placed_item(item.item_id, item.zone_title[i])
reset.click()
def _get_scenario_xml(self):
return self._get_custom_scenario_xml("data/test_multiple_options_data.json")
@ddt
class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegrationTest):
"""
......@@ -339,7 +367,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
},
{
'name': 'grade',
'data': {'max_value': 1, 'value': (1.0 / 3)},
'data': {'max_value': 1, 'value': (1.0 / 4)},
},
{
'name': 'edx.drag_and_drop_v2.item.dropped',
......@@ -409,9 +437,9 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
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, None, "", "No Zone for this")
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']
......@@ -427,9 +455,9 @@ class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = {
0: ItemDefinition(0, 'zone-1', 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, 'zone-2', 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>"),
2: ItemDefinition(2, None, None, "", "No Zone for <i>X</i>")
0: ItemDefinition(0, ['zone-1'], 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"),
1: ItemDefinition(1, ['zone-2'], 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>"),
2: ItemDefinition(2, [], None, "", "No Zone for <i>X</i>")
}
all_zones = ['zone-1', 'zone-2']
......@@ -452,14 +480,14 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
item_maps = {
'block1': {
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, None, "", "No Zone for this")
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")
},
'block2': {
10: ItemDefinition(10, 'zone-51', 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, 'zone-52', 'Zone 52', "Correct 2", "Incorrect 2"),
30: ItemDefinition(30, None, None, "", "No Zone for this")
10: ItemDefinition(10, ['zone-51'], 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, ['zone-52'], 'Zone 52', "Correct 2", "Incorrect 2"),
30: ItemDefinition(30, [], None, "", "No Zone for this")
},
}
......
......@@ -27,7 +27,7 @@
"incorrect": "No <b>1</b>",
"correct": "Yes <b>1</b>"
},
"zone": "Zone <i>1</i>",
"zones": ["Zone <i>1</i>"],
"imageURL": "",
"id": 0
},
......@@ -37,7 +37,10 @@
"incorrect": "No <i>2</i>",
"correct": "Yes <i>2</i>"
},
"zone": "Zone <b>2</b>",
"zones": [
"Zone <b>2</b>",
"Zone <i>1</i>"
],
"imageURL": "",
"id": 1
},
......@@ -47,7 +50,7 @@
"incorrect": "",
"correct": ""
},
"zone": "none",
"zones": [],
"imageURL": "",
"id": 2
},
......@@ -57,7 +60,7 @@
"incorrect": "",
"correct": ""
},
"zone": "none",
"zones": [],
"imageURL": "http://placehold.it/100x300",
"id": 3
}
......
......@@ -26,7 +26,7 @@
"incorrect": "No 1",
"correct": "Yes 1"
},
"zone": "zone-1",
"zones": ["zone-1"],
"imageURL": "",
"id": 0
},
......@@ -36,7 +36,10 @@
"incorrect": "No 2",
"correct": "Yes 2"
},
"zone": "zone-2",
"zones": [
"zone-2",
"zone-1"
],
"imageURL": "",
"id": 1
},
......@@ -46,7 +49,7 @@
"incorrect": "",
"correct": ""
},
"zone": "none",
"zones": [],
"imageURL": "/static/test_url_expansion",
"id": 2
},
......@@ -56,7 +59,7 @@
"incorrect": "",
"correct": ""
},
"zone": "none",
"zones": [],
"imageURL": "http://placehold.it/200x100",
"id": 3
}
......
......@@ -48,7 +48,13 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(items, [
{"id": i, "displayName": display_name, "imageURL": "", "expandedImageURL": ""}
for i, display_name in enumerate(
["Goes to the top", "Goes to the middle", "Goes to the bottom", "I don't belong anywhere"]
[
"Goes to the top",
"Goes to the middle",
"Goes to the bottom",
"Goes anywhere",
"I don't belong anywhere"
]
)
])
......@@ -72,6 +78,8 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.call_handler('do_attempt', data)
data = {"val": 2, "zone": BOTTOM_ZONE_ID, "x_percent": "99%", "y_percent": "95%"}
self.call_handler('do_attempt', data)
data = {"val": 3, "zone": MIDDLE_ZONE_ID, "x_percent": "67%", "y_percent": "80%"}
self.call_handler('do_attempt', data)
# Check the result:
self.assertTrue(self.block.completed)
......@@ -79,12 +87,14 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct': True, 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct': True, 'zone': BOTTOM_ZONE_ID},
'3': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, "zone": MIDDLE_ZONE_ID},
})
self.assertEqual(self.call_handler('get_user_state'), {
'items': {
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct': True, 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct': True, 'zone': BOTTOM_ZONE_ID},
'3': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, "zone": MIDDLE_ZONE_ID},
},
'finished': True,
'overall_feedback': 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