Commit 7c2f0846 by Eugeny Kolpakov Committed by GitHub

Merge pull request #75 from open-craft/ekolpakov/mode_field

Mode field
parents 2a9bc5a9 ba559f81
...@@ -13,7 +13,6 @@ The editor is fully guided. Features include: ...@@ -13,7 +13,6 @@ The editor is fully guided. Features include:
* custom text and background colors for items * custom text and background colors for items
* optional auto-alignment for items (left, right, center) * optional auto-alignment for items (left, right, center)
* image items * image items
* items prompting for additional (numerical) input after being dropped
* 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
...@@ -24,12 +23,12 @@ refreshes. All checking and record keeping is done on the server side. ...@@ -24,12 +23,12 @@ refreshes. All checking and record keeping is done on the server side.
The following screenshot shows the Drag and Drop XBlock rendered The following screenshot shows the Drag and Drop XBlock rendered
inside the edX LMS 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/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-start.png) ![Student view start](/doc/img/student-view-start.png)
This screenshot shows the XBlock after the learner successfully This screenshot shows the XBlock after the learner 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/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-finish.png) ![Student view finish](/doc/img/student-view-finish.png)
Installation Installation
------------ ------------
...@@ -95,15 +94,15 @@ Usage ...@@ -95,15 +94,15 @@ 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/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view.png) ![Edit view](/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, the maximum score, the problem text to render such as the title, problem mode (Standard vs. Assessment), the maximum score,
above the background image, the introductory feedback (shown the problem text to render above the background image, the introductory feedback (shown
initially), and the final feedback (shown after the learner initially), and the final feedback (shown after the learner
successfully completes the drag and drop problem). successfully completes the drag and drop problem).
![Drop zone edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/ebd0b52d971bbf93b9c3873f310bd72d336d865b/doc/img/edit-view-zones.png) ![Drop zone edit](/doc/img/edit-view-zones.png)
In the next step, you set the URL and description for the background In the next step, you set the URL and description for the background
image and define the properties of the drop zones. For each zone you image and define the properties of the drop zones. For each zone you
...@@ -125,7 +124,7 @@ items dropped in a zone will not overlap, but if the zone is not made large ...@@ -125,7 +124,7 @@ items dropped in a zone will not overlap, but if the zone is not made large
enough for all its items, they will overflow the bottom of the zone, and enough for all its items, they will overflow the bottom of the zone, and
potentially, overlap the zones below. potentially, overlap the zones below.
![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-items.png) ![Drag item edit](/doc/img/edit-view-items.png)
In the final step, you define the background and text color for drag In the final step, you define the background and text color for drag
items, as well as the drag items themselves. A drag item can contain items, as well as the drag items themselves. A drag item can contain
...@@ -135,16 +134,7 @@ after the learner drops the item on a zone - the success feedback is ...@@ -135,16 +134,7 @@ 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 the correct zone, while the error
feedback is shown when dropping the item on an incorrect drop zone. feedback is shown when dropping the item on an incorrect drop zone.
Additionally, items can have a numerical value (and an optional error ![Zone dropdown](/doc/img/edit-view-zone-dropdown.png)
margin) associated with them. When a learner 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 learner submits is
checked against the expected value for the item. If you also specify a
margin, the value entered by the learner 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/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-zone-dropdown.png)
The zone that an item belongs to 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`
...@@ -153,6 +143,13 @@ any zone. ...@@ -153,6 +143,13 @@ any zone.
You can define an arbitrary number of drag items. You can define an arbitrary number of drag items.
Demo Course
-----------
Export of a demo course that showcases various Drag and Drop v2
features is available at
[github.com/open-craft/demo-courses/archive/drag-and-drop-v2.tar.gz](https://github.com/open-craft/demo-courses/archive/drag-and-drop-v2.tar.gz).
Analytics Events Analytics Events
---------------- ----------------
...@@ -261,9 +258,7 @@ Example ("common" fields that are not interesting in this context have been left ...@@ -261,9 +258,7 @@ Example ("common" fields that are not interesting in this context have been left
{ {
... ...
"event": { "event": {
"input": null, "is_correct": true, -- Whether the draggable item has been placed in the correct location.
"is_correct": true, -- False if there is an input in the draggable item, and the learner provided the wrong answer. Otherwise true.
"is_correct_location": true, -- Whether the draggable item has been placed in the correct location.
"item_id": 0, -- ID of the draggable item. "item_id": 0, -- ID of the draggable item.
"location": "The Top Zone", -- Name of the location the item was dragged to. "location": "The Top Zone", -- Name of the location the item was dragged to.
}, },
...@@ -284,11 +279,9 @@ Real event example (taken from a devstack): ...@@ -284,11 +279,9 @@ Real event example (taken from a devstack):
"referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280", "referer": "http://example.com/courses/course-v1:DnD+DnD+DnD/courseware/ec546c58d2f447b7a9223c57b5de7344/756071f8de7f47c3b0ae726586ebbe16/1?activate_block_id=block-v1%3ADnD%2BDnD%2BDnD%2Btype%40vertical%2Bblock%40d2fc47476ca14c55816c4a1264a27280",
"accept_language": "en;q=1.0, en;q=0.5", "accept_language": "en;q=1.0, en;q=0.5",
"event": { "event": {
"is_correct_location": true,
"is_correct": true, "is_correct": true,
"location": "The Top Zone", "location": "The Top Zone",
"item_id": 0, "item_id": 0,
"input": null
}, },
"event_source": "server", "event_source": "server",
"context": { "context": {
......
doc/img/edit-view.png

27.5 KB | W: | H:

doc/img/edit-view.png

32.2 KB | W: | H:

doc/img/edit-view.png
doc/img/edit-view.png
doc/img/edit-view.png
doc/img/edit-view.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -32,6 +32,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -32,6 +32,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
""" """
XBlock that implements a friendly Drag-and-Drop problem XBlock that implements a friendly Drag-and-Drop problem
""" """
STANDARD_MODE = "standard"
ASSESSMENT_MODE = "assessment"
display_name = String( display_name = String(
display_name=_("Title"), display_name=_("Title"),
help=_("The title of the drag and drop problem. The title is displayed to learners."), help=_("The title of the drag and drop problem. The title is displayed to learners."),
...@@ -39,6 +42,20 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -39,6 +42,20 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
default=_("Drag and Drop"), default=_("Drag and Drop"),
) )
mode = String(
display_name=_("Mode"),
help=_(
"Standard mode: feedback is provided to learner right after an item is dropped to a zone. "
"Assessment mode: learner must place all the items to zones to see the feedback."
),
scope=Scope.settings,
values=[
{"display_name": _("Standard"), "value": STANDARD_MODE},
{"display_name": _("Assessment"), "value": ASSESSMENT_MODE},
],
default=STANDARD_MODE
)
show_title = Boolean( show_title = Boolean(
display_name=_("Show title"), display_name=_("Show title"),
help=_("Display the title to the learner?"), help=_("Display the title to the learner?"),
...@@ -148,7 +165,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -148,7 +165,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
for item in items: for item in items:
del item['feedback'] del item['feedback']
del item['zone'] del item['zone']
item['inputOptions'] = 'inputOptions' in item
# Fall back on "backgroundImage" to be backward-compatible. # Fall back on "backgroundImage" to be backward-compatible.
image_url = item.get('imageURL') or item.get('backgroundImage') image_url = item.get('imageURL') or item.get('backgroundImage')
if image_url: if image_url:
...@@ -186,9 +202,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -186,9 +202,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
field_name: self.ugettext(field.help) field_name: self.ugettext(field.help)
for field_name, field in self.fields.viewitems() if hasattr(field, "help") for field_name, field in self.fields.viewitems() if hasattr(field, "help")
} }
field_values = {
field_name: field.values
for field_name, field in self.fields.viewitems() if hasattr(field, "values")
}
context = { context = {
'js_templates': js_templates, 'js_templates': js_templates,
'help_texts': help_texts, 'help_texts': help_texts,
'field_values': field_values,
'self': self, 'self': self,
'data': urllib.quote(json.dumps(self.data)), 'data': urllib.quote(json.dumps(self.data)),
} }
...@@ -221,6 +242,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -221,6 +242,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
@XBlock.json_handler @XBlock.json_handler
def studio_submit(self, submissions, suffix=''): def studio_submit(self, submissions, suffix=''):
self.display_name = submissions['display_name'] self.display_name = submissions['display_name']
self.mode = submissions['mode']
self.show_title = submissions['show_title'] self.show_title = submissions['show_title']
self.question_text = submissions['problem_text'] self.question_text = submissions['problem_text']
self.show_question_header = submissions['show_problem_header'] self.show_question_header = submissions['show_problem_header']
...@@ -242,31 +264,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -242,31 +264,13 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback = item['feedback']['incorrect'] feedback = item['feedback']['incorrect']
overall_feedback = None overall_feedback = None
is_correct = False is_correct = False
is_correct_location = False
if item['zone'] == attempt['zone']: # Student placed item in correct zone
if 'input' in attempt: # Student submitted numerical value for item is_correct = True
state = self._get_item_state().get(str(item['id'])) feedback = item['feedback']['correct']
if state:
state['input'] = attempt['input']
is_correct_location = True
if self._is_correct_input(item, attempt['input']):
is_correct = True
feedback = item['feedback']['correct']
else:
is_correct = False
elif item['zone'] == attempt['zone']: # Student placed item in correct zone
is_correct_location = True
if 'inputOptions' in item:
# Input value will have to be provided for the item.
# It is not (yet) correct and no feedback should be shown yet.
is_correct = False
feedback = None
else:
# If this item has no input value set, we are done with it.
is_correct = True
feedback = item['feedback']['correct']
state = { state = {
'zone': attempt['zone'], 'zone': attempt['zone'],
'correct': True,
'x_percent': attempt['x_percent'], 'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'], 'y_percent': attempt['y_percent'],
} }
...@@ -300,14 +304,11 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -300,14 +304,11 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'item_id': item['id'], 'item_id': item['id'],
'location': zone.get("title"), 'location': zone.get("title"),
'location_id': zone.get("uid"), 'location_id': zone.get("uid"),
'input': attempt.get('input'),
'is_correct_location': is_correct_location,
'is_correct': is_correct, 'is_correct': is_correct,
}) })
return { return {
'correct': is_correct, 'correct': is_correct,
'correct_location': is_correct_location,
'finished': self._is_finished(), 'finished': self._is_finished(),
'overall_feedback': overall_feedback, 'overall_feedback': overall_feedback,
'feedback': feedback 'feedback': feedback
...@@ -371,7 +372,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -371,7 +372,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
item_state = self._get_item_state() item_state = self._get_item_state()
for item_id, item in item_state.iteritems(): for item_id, item in item_state.iteritems():
definition = self._get_item_definition(int(item_id)) definition = self._get_item_definition(int(item_id))
item['correct_input'] = self._is_correct_input(definition, item.get('input'))
# If information about zone is missing # If information about zone is missing
# (because problem was completed before a11y enhancements were implemented), # (because problem was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition: # deduce zone in which item is placed from definition:
...@@ -443,8 +443,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -443,8 +443,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
total_count += 1 total_count += 1
item_id = str(item['id']) item_id = str(item['id'])
if item_id in item_state: if item_id in item_state:
if self._is_correct_input(item, item_state[item_id].get('input')): correct_count += 1
correct_count += 1
return correct_count / float(total_count) * self.weight return correct_count / float(total_count) * self.weight
...@@ -461,11 +460,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -461,11 +460,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
total_count += 1 total_count += 1
item_id = str(item['id']) item_id = str(item['id'])
if item_id in item_state: if item_id in item_state:
if 'inputOptions' in item: completed_count += 1
if 'input' in item_state[item_id]:
completed_count += 1
else:
completed_count += 1
return completed_count == total_count return completed_count == total_count
...@@ -488,25 +483,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin): ...@@ -488,25 +483,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return usage_id return usage_id
@staticmethod @staticmethod
def _is_correct_input(item, val):
"""
Is submitted numerical value within the tolerated margin for this item.
"""
input_options = item.get('inputOptions')
if input_options:
try:
submitted_value = float(val)
except (ValueError, TypeError):
return False
else:
expected_value = input_options['value']
margin = input_options['margin']
return abs(submitted_value - expected_value) <= margin
else:
return True
@staticmethod
def workbench_scenarios(): def workbench_scenarios():
""" """
A canned scenario for display in the workbench. A canned scenario for display in the workbench.
......
...@@ -193,49 +193,6 @@ ...@@ -193,49 +193,6 @@
max-width: 100%; max-width: 100%;
} }
.xblock--drag-and-drop .drag-container .option .numerical-input {
height: 32px;
position: absolute;
left: calc(100% + 5px);
top: calc(50% - 16px);
}
.xblock--drag-and-drop .drag-container .option .numerical-input .spinner-wrapper {
position: absolute;
right: 0;
top: 0;
margin-right: 5px;
padding-top: 6px;
color: #333;
}
.xblock--drag-and-drop .drag-container .option .numerical-input .input {
display: inline-block;
width: 144px;
}
.xblock--drag-and-drop .drag-container .option .numerical-input .submit-input {
position: absolute;
left: 150px;
top: 4px;
white-space: nowrap; /* Fix cross-browser issue: Without this declaration, button text wraps in Chrome/Chromium */
}
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input-submit,
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input-submit {
display: none;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background-color: #ceffce;
color: #087108;
}
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background-color: #ffcece;
color: #ad0d0d;
}
.xblock--drag-and-drop .drag-container .option.fade { .xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.80; opacity: 0.80;
} }
......
...@@ -196,12 +196,6 @@ ...@@ -196,12 +196,6 @@
width: 50px; width: 50px;
} }
.xblock--drag-and-drop--editor .items-form .item-numerical-value,
.xblock--drag-and-drop--editor .items-form .item-numerical-margin {
margin: 0 1%;
width: 50%;
}
.xblock--drag-and-drop--editor .items-form textarea { .xblock--drag-and-drop--editor .items-form textarea {
width: 97%; width: 97%;
margin: 0 1%; margin: 0 1%;
......
...@@ -4,20 +4,6 @@ function DragNDropTemplates(url_name) { ...@@ -4,20 +4,6 @@ function DragNDropTemplates(url_name) {
// Set up a mock for gettext if it isn't available in the client runtime: // 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; }; } if (!window.gettext) { window.gettext = function gettext_stub(string) { return string; }; }
var FocusHook = function() {
if (!(this instanceof FocusHook)) {
return new FocusHook();
}
};
FocusHook.prototype.hook = function(node, prop, prev) {
setTimeout(function() {
if (document.activeElement !== node) {
node.focus();
}
}, 0);
};
var itemSpinnerTemplate = function(xhr_active) { var itemSpinnerTemplate = function(xhr_active) {
if (!xhr_active) { if (!xhr_active) {
return null; return null;
...@@ -36,22 +22,6 @@ function DragNDropTemplates(url_name) { ...@@ -36,22 +22,6 @@ function DragNDropTemplates(url_name) {
}); });
}; };
var itemInputTemplate = function(input) {
if (!input) {
return null;
}
var focus_hook = input.has_value ? undefined : FocusHook();
return (
h('div.numerical-input', {className: input.class_name,
style: {display: input.is_visible ? 'block' : 'none'}}, [
h('input.input', {type: 'text', value: input.value, disabled: input.has_value,
focusHook: focus_hook}),
itemSpinnerTemplate(input.xhr_active),
h('button.submit-input', {disabled: input.has_value}, gettext('ok'))
])
);
};
var getZone = function(zoneUID, ctx) { var getZone = function(zoneUID, ctx) {
for (var i = 0; i < ctx.zones.length; i++) { for (var i = 0; i < ctx.zones.length; i++) {
if (ctx.zones[i].uid === zoneUID) { if (ctx.zones[i].uid === zoneUID) {
...@@ -98,10 +68,6 @@ function DragNDropTemplates(url_name) { ...@@ -98,10 +68,6 @@ function DragNDropTemplates(url_name) {
} }
} else { } else {
// This is an "aligned" zone, so the item position within the zone is calculated by the browser. // This is an "aligned" zone, so the item position within the zone is calculated by the browser.
// Allow for the input + button width for aligned items
if (item.input) {
style.marginRight = '190px';
}
// Make up for the fact we're in a wrapper container by calculating percentage differences. // Make up for the fact we're in a wrapper container by calculating percentage differences.
var maxWidth = (item.widthPercent || 30) / 100; var maxWidth = (item.widthPercent || 30) / 100;
var widthPercent = zone.width_percent / 100; var widthPercent = zone.width_percent / 100;
...@@ -132,8 +98,7 @@ function DragNDropTemplates(url_name) { ...@@ -132,8 +98,7 @@ function DragNDropTemplates(url_name) {
} }
// Define children // Define children
var children = [ var children = [
itemSpinnerTemplate(item.xhr_active), itemSpinnerTemplate(item.xhr_active)
itemInputTemplate(item.input)
]; ];
var item_content_html = item.displayName; var item_content_html = item.displayName;
if (item.imageURL) { if (item.imageURL) {
...@@ -386,7 +351,6 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -386,7 +351,6 @@ function DragAndDropBlock(runtime, element, configuration) {
$element.on('keydown', '.reset-button', function(evt) { $element.on('keydown', '.reset-button', function(evt) {
runOnKey(evt, RET, resetProblem); runOnKey(evt, RET, resetProblem);
}); });
$element.on('click', '.submit-input', submitInput);
// For the next one, we need to use addEventListener with useCapture 'true' in order // For the next one, we need to use addEventListener with useCapture 'true' in order
// to watch for load events on any child element, since load events do not bubble. // to watch for load events on any child element, since load events do not bubble.
...@@ -783,9 +747,9 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -783,9 +747,9 @@ function DragAndDropBlock(runtime, element, configuration) {
$.post(url, JSON.stringify(data), 'json') $.post(url, JSON.stringify(data), 'json')
.done(function(data){ .done(function(data){
state.last_action_correct = data.correct_location; state.last_action_correct = data.correct;
if (data.correct_location) { if (data.correct) {
state.items[item_id].correct_input = Boolean(data.correct); state.items[item_id].correct = true;
state.items[item_id].submitting_location = false; state.items[item_id].submitting_location = false;
} else { } else {
delete state.items[item_id]; delete state.items[item_id];
...@@ -803,42 +767,6 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -803,42 +767,6 @@ function DragAndDropBlock(runtime, element, configuration) {
}); });
}; };
var submitInput = function(evt) {
var item = $(evt.target).closest('.option');
var input_div = item.find('.numerical-input');
var input = input_div.find('.input');
var input_value = input.val();
var item_id = item.data('value');
if (!input_value) {
// Don't submit if the user didn't enter anything yet.
return;
}
state.items[item_id].input = input_value;
state.items[item_id].submitting_input = true;
applyState();
var url = runtime.handlerUrl(element, 'do_attempt');
var data = {val: item_id, input: input_value};
$.post(url, JSON.stringify(data), 'json')
.done(function(data) {
state.last_action_correct = data.correct;
state.items[item_id].submitting_input = false;
state.items[item_id].correct_input = data.correct;
state.feedback = data.feedback;
if (data.finished) {
state.finished = true;
state.overall_feedback = data.overall_feedback;
}
applyState();
})
.fail(function(data) {
state.items[item_id].submitting_input = false;
applyState();
});
};
var closePopup = function(evt) { var closePopup = function(evt) {
if (!state.feedback) { if (!state.feedback) {
return; return;
...@@ -847,9 +775,8 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -847,9 +775,8 @@ function DragAndDropBlock(runtime, element, configuration) {
var target = $(evt.target); var target = $(evt.target);
var popup_box = '.xblock--drag-and-drop .popup'; var popup_box = '.xblock--drag-and-drop .popup';
var close_button = '.xblock--drag-and-drop .popup .close'; var close_button = '.xblock--drag-and-drop .popup .close';
var submit_input_button = '.xblock--drag-and-drop .submit-input';
if (target.is(popup_box) || target.is(submit_input_button)) { if (target.is(popup_box)) {
return; return;
} }
if (target.parents(popup_box).length && !target.is(close_button)) { if (target.parents(popup_box).length && !target.is(close_button)) {
...@@ -884,31 +811,17 @@ function DragAndDropBlock(runtime, element, configuration) { ...@@ -884,31 +811,17 @@ function DragAndDropBlock(runtime, element, configuration) {
var render = function() { var render = function() {
var items = configuration.items.map(function(item) { var items = configuration.items.map(function(item) {
var input = null;
var item_user_state = state.items[item.id]; var item_user_state = state.items[item.id];
if (item.inputOptions) {
input = {
is_visible: item_user_state && !item_user_state.submitting_location,
has_value: Boolean(item_user_state && 'input' in item_user_state),
value : (item_user_state && item_user_state.input) || '',
class_name: undefined,
xhr_active: (item_user_state && item_user_state.submitting_input)
};
if (input.has_value && !item_user_state.submitting_input) {
input.class_name = item_user_state.correct_input ? 'correct' : 'incorrect';
}
}
var grabbed = false; var grabbed = false;
if (item.grabbed !== undefined) { if (item.grabbed !== undefined) {
grabbed = item.grabbed; grabbed = item.grabbed;
} }
var placed = item_user_state && ('input' in item_user_state || item_user_state.correct_input); var placed = item_user_state && item_user_state.correct;
var itemProperties = { var itemProperties = {
value: item.id, value: item.id,
drag_disabled: Boolean(item_user_state || state.finished), drag_disabled: Boolean(item_user_state || state.finished),
class_name: placed || state.finished ? 'fade' : undefined, class_name: placed || state.finished ? 'fade' : undefined,
xhr_active: (item_user_state && item_user_state.submitting_location), xhr_active: (item_user_state && item_user_state.submitting_location),
input: input,
displayName: item.displayName, displayName: item.displayName,
imageURL: item.expandedImageURL, imageURL: item.expandedImageURL,
imageDescription: item.imageDescription, imageDescription: item.imageDescription,
......
...@@ -414,10 +414,6 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -414,10 +414,6 @@ function DragAndDropEditBlock(runtime, element, params) {
// block, but preserve the data in case we need it again: // block, but preserve the data in case we need it again:
ctx.pixelHeight = itemData.size.height.substr(0, itemData.size.height.length - 2); // Remove 'px' ctx.pixelHeight = itemData.size.height.substr(0, itemData.size.height.length - 2); // Remove 'px'
} }
if (itemData.inputOptions) {
ctx.numericalValue = itemData.inputOptions.value;
ctx.numericalMargin = itemData.inputOptions.margin;
}
} }
ctx.dropdown = _fn.build.form.createDropdown(ctx.zone); ctx.dropdown = _fn.build.form.createDropdown(ctx.zone);
...@@ -486,15 +482,6 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -486,15 +482,6 @@ function DragAndDropEditBlock(runtime, element, params) {
var widthPercent = $el.find('.item-width').val(); var widthPercent = $el.find('.item-width').val();
if (widthPercent && +widthPercent > 0) { data.widthPercent = widthPercent; } if (widthPercent && +widthPercent > 0) { data.widthPercent = widthPercent; }
var numValue = parseFloat($el.find('.item-numerical-value').val());
var numMargin = parseFloat($el.find('.item-numerical-margin').val());
if (isFinite(numValue)) {
data.inputOptions = {
value: numValue,
margin: isFinite(numMargin) ? numMargin : 0
};
}
items.push(data); items.push(data);
} }
}); });
...@@ -504,6 +491,7 @@ function DragAndDropEditBlock(runtime, element, params) { ...@@ -504,6 +491,7 @@ function DragAndDropEditBlock(runtime, element, params) {
var data = { var data = {
'display_name': $element.find('#display-name').val(), 'display_name': $element.find('#display-name').val(),
'mode': $element.find("#problem-mode").val(),
'show_title': $element.find('.show-title').is(':checked'), 'show_title': $element.find('.show-title').is(':checked'),
'weight': $element.find('#weight').val(), 'weight': $element.find('#weight').val(),
'problem_text': $element.find('#problem-text').val(), 'problem_text': $element.find('#problem-text').val(),
......
...@@ -32,16 +32,6 @@ ...@@ -32,16 +32,6 @@
opacity: 1; opacity: 1;
} }
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background-color: #ceffce;
color: #087108;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background-color: #ffcece;
color: #ad0d0d;
}
.themed-xblock.xblock--drag-and-drop .drag-container .option.fade { .themed-xblock.xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.5; opacity: 0.5;
} }
...@@ -85,4 +75,4 @@ ...@@ -85,4 +75,4 @@
.themed-xblock.xblock--drag-and-drop .link-button { .themed-xblock.xblock--drag-and-drop .link-button {
cursor: pointer; cursor: pointer;
color: #3384ca; color: #3384ca;
} }
\ No newline at end of file
...@@ -21,6 +21,16 @@ ...@@ -21,6 +21,16 @@
<span class="sr">{{ help_texts.show_title }}</span> <span class="sr">{{ help_texts.show_title }}</span>
</label> </label>
<label class="h3" for="problem-mode" title="{{ help_texts.mode }}">{% trans "Problem mode" %}</label>
<select id="problem-mode">
{% for field_value in field_values.mode %}
<option value="{{ field_value.value }}" {% if self.mode == field_value.value %}selected{% endif %}>
{{ field_value.display_name }}
</option>
{% endfor %}
</select>
<span class="sr">{{ help_texts.mode }}</span>
<label class="h3" for="weight">{% trans "Maximum score" %}</label> <label class="h3" for="weight">{% trans "Maximum score" %}</label>
<input id="weight" type="number" step="0.1" value="{{ self.weight|unlocalize }}" /> <input id="weight" type="number" step="0.1" value="{{ self.weight|unlocalize }}" />
......
...@@ -131,23 +131,5 @@ ...@@ -131,23 +131,5 @@
<label for="item-{{id}}-width-percent">{{i18n "Preferred width as a percentage of the background image width (or blank for automatic width):"}}</label> <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" />% <input type="number" id="item-{{id}}-width-percent" class="item-width" value="{{ singleDecimalFloat widthPercent }}" step="0.1" min="1" max="99" />%
</div> </div>
<div class="row advanced">
<label for="item-{{id}}-numerical-value">
{{i18n "Optional numerical value (if you set this, learners will be prompted for this value after dropping this item)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-value"
class="item-numerical-value" value="{{ numericalValue }}" />
</div>
<div class="row advanced">
<label for="item-{{id}}-numerical-margin">
{{i18n "Margin ± (when a numerical value is required, values entered by learners must not differ from the expected value by more than this margin; default is zero)"}}
</label>
<input type="number"
step="0.1"
id="item-{{id}}-numerical-margin"
class="item-numerical-margin" value="{{ numericalMargin }}" />
</div>
</div> </div>
</script> </script>
...@@ -248,19 +248,6 @@ msgid "" ...@@ -248,19 +248,6 @@ msgid ""
"automatic width):" "automatic width):"
msgstr "" msgstr ""
#: templates/html/js_templates.html
msgid ""
"Optional numerical value (if you set this, learners will be prompted for "
"this value after dropping this item)"
msgstr ""
#: templates/html/js_templates.html
msgid ""
"Margin ± (when a numerical value is required, values entered by learners "
"must not differ from the expected value by more than this margin; default "
"is zero)"
msgstr ""
#: templates/html/drag_and_drop.html #: templates/html/drag_and_drop.html
msgid "Loading drag and drop problem." msgid "Loading drag and drop problem."
msgstr "" msgstr ""
......
...@@ -304,29 +304,6 @@ msgstr "" ...@@ -304,29 +304,6 @@ 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 " "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): Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#" "äütömätïç wïdth): Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σ#"
#: templates/html/js_templates.html
msgid ""
"Optional numerical value (if you set this, learners will be prompted for "
"this value after dropping this item)"
msgstr ""
"Öptïönäl nümérïçäl välüé (ïf ýöü sét thïs, léärnérs wïll ßé prömptéd för "
"thïs välüé äftér dröppïng thïs ïtém) Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: templates/html/js_templates.html
msgid ""
"Margin ± (when a numerical value is required, values entered by learners "
"must not differ from the expected value by more than this margin; default is"
" zero)"
msgstr ""
"Märgïn ± (whén ä nümérïçäl välüé ïs réqüïréd, välüés éntéréd ßý léärnérs "
"müst nöt dïffér fröm thé éxpéçtéd välüé ßý möré thän thïs märgïn; défäült ïs"
" zérö) Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι¢ιηg єłιт, ѕє∂ ∂σ "
"єιυѕмσ∂ тємρσя ιη¢ι∂ι∂υηт υт łαвσяє єт ∂σłσяє мαgηα αłιqυα. υт єηιм α∂ мιηιм"
" νєηιαм, qυιѕ ησѕтяυ∂ єχєя¢ιтαтιση υłłαм¢σ łαвσяιѕ ηιѕι υт αłιqυιρ єχ єα "
"¢σммσ∂σ ¢σηѕєqυαт. ∂υιѕ αυтє ιяυяє ∂σłσя ιη яєρяєнєη∂єяιт ιη νσłυρтαтє νєłιт"
" єѕѕє ¢ιłłυм ∂σłσяє єυ ƒυgιαт ηυłłα ραяιαтυя. єχ¢єρтєυя ѕιηт σ¢¢αє¢αт "
"¢υρι∂αтαт ηση ρяσι∂єηт, ѕυηт ιη ¢υłρα qυι σƒƒι¢ια ∂єѕєяυηт мσ#"
#: templates/html/drag_and_drop.html #: templates/html/drag_and_drop.html
msgid "Loading drag and drop problem." msgid "Loading drag and drop problem."
msgstr "Löädïng dräg änd dröp prößlém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#" msgstr "Löädïng dräg änd dröp prößlém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
......
...@@ -20,4 +20,4 @@ disable= ...@@ -20,4 +20,4 @@ disable=
no-member no-member
[SIMILARITIES] [SIMILARITIES]
min-similarity-lines=8 min-similarity-lines=4
...@@ -47,10 +47,6 @@ ...@@ -47,10 +47,6 @@
"size": { "size": {
"width": "190px", "width": "190px",
"height": "100px" "height": "100px"
},
"inputOptions": {
"value": 100,
"margin": 5
} }
}, },
{ {
...@@ -77,4 +73,4 @@ ...@@ -77,4 +73,4 @@
"finish": "Final Feedback" "finish": "Final Feedback"
}, },
"title": "Drag and Drop (Old-style data)" "title": "Drag and Drop (Old-style data)"
} }
\ No newline at end of file
...@@ -36,11 +36,7 @@ ...@@ -36,11 +36,7 @@
}, },
"zone": "zone-2", "zone": "zone-2",
"imageURL": "", "imageURL": "",
"id": 1, "id": 1
"inputOptions": {
"value": 100,
"margin": 5
}
}, },
{ {
"displayName": "X", "displayName": "X",
......
...@@ -36,11 +36,7 @@ ...@@ -36,11 +36,7 @@
}, },
"zone": "zone-52", "zone": "zone-52",
"imageURL": "", "imageURL": "",
"id": 20, "id": 20
"inputOptions": {
"value": 100,
"margin": 5
}
}, },
{ {
"displayName": "X", "displayName": "X",
......
...@@ -36,11 +36,7 @@ ...@@ -36,11 +36,7 @@
}, },
"zone": "zone-2", "zone": "zone-2",
"imageURL": "", "imageURL": "",
"id": 1, "id": 1
"inputOptions": {
"value": 100,
"margin": 5
}
}, },
{ {
"displayName": "<span style='color:red'>X</span>", "displayName": "<span style='color:red'>X</span>",
......
...@@ -22,7 +22,6 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest): ...@@ -22,7 +22,6 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
self.assertEqual(len(items), 3) self.assertEqual(len(items), 3)
self.assertIn('<b>1</b>', self.get_element_html(items[0])) self.assertIn('<b>1</b>', self.get_element_html(items[0]))
self.assertIn('<i>2</i>', self.get_element_html(items[1])) self.assertIn('<i>2</i>', self.get_element_html(items[1]))
self.assertIn('<input class="input" type="text">', self.get_element_html(items[1]))
self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2])) self.assertIn('<span style="color:red">X</span>', self.get_element_html(items[2]))
def test_background_image(self): def test_background_image(self):
......
...@@ -27,13 +27,12 @@ loader = ResourceLoader(__name__) ...@@ -27,13 +27,12 @@ loader = ResourceLoader(__name__)
# Classes ########################################################### # Classes ###########################################################
class ItemDefinition(object): class ItemDefinition(object):
def __init__(self, item_id, zone_id, zone_title, feedback_positive, feedback_negative, input_value=None): def __init__(self, item_id, zone_id, zone_title, feedback_positive, feedback_negative):
self.feedback_negative = feedback_negative self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive self.feedback_positive = feedback_positive
self.zone_id = zone_id self.zone_id = zone_id
self.zone_title = zone_title self.zone_title = zone_title
self.item_id = item_id self.item_id = item_id
self.input = input_value
class InteractionTestBase(object): class InteractionTestBase(object):
...@@ -77,10 +76,6 @@ class InteractionTestBase(object): ...@@ -77,10 +76,6 @@ class InteractionTestBase(object):
zones_container = self._page.find_element_by_css_selector('.target') zones_container = self._page.find_element_by_css_selector('.target')
return zones_container.find_elements_by_xpath(".//div[@data-uid='{zone_id}']".format(zone_id=zone_id))[0] return zones_container.find_elements_by_xpath(".//div[@data-uid='{zone_id}']".format(zone_id=zone_id))[0]
def _get_input_div_by_value(self, item_value):
element = self._get_item_by_value(item_value)
return element.find_element_by_class_name('numerical-input')
def _get_dialog_components(self, dialog): # pylint: disable=no-self-use def _get_dialog_components(self, dialog): # pylint: disable=no-self-use
dialog_modal_overlay = dialog.find_element_by_css_selector('.modal-window-overlay') dialog_modal_overlay = dialog.find_element_by_css_selector('.modal-window-overlay')
dialog_modal = dialog.find_element_by_css_selector('.modal-window') dialog_modal = dialog.find_element_by_css_selector('.modal-window')
...@@ -119,17 +114,6 @@ class InteractionTestBase(object): ...@@ -119,17 +114,6 @@ class InteractionTestBase(object):
self._page.send_keys(Keys.TAB) self._page.send_keys(Keys.TAB)
self._get_zone_by_id(zone_id).send_keys(action_key) self._get_zone_by_id(zone_id).send_keys(action_key)
def send_input(self, item_value, value):
element = self._get_item_by_value(item_value)
self.wait_until_visible(element)
# Since virtual-dom may be updating DOM elements by replacing them completely, the
# following method must be used to wait for the input box to appear:
textbox_visible_selector = '.numerical-input[style*="display: block"] input'
self.wait_until_exists(textbox_visible_selector)
textbox = self._page.find_element_by_css_selector(textbox_visible_selector)
textbox.send_keys(value)
element.find_element_by_class_name('submit-input').click()
def assert_grabbed_item(self, item): def assert_grabbed_item(self, item):
self.assertEqual(item.get_attribute('aria-grabbed'), 'true') self.assertEqual(item.get_attribute('aria-grabbed'), 'true')
...@@ -185,28 +169,10 @@ class InteractionTestBase(object): ...@@ -185,28 +169,10 @@ class InteractionTestBase(object):
self.scroll_down(pixels=scroll_down) self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values(): for definition in self._get_items_with_zone(items_map).values():
if not definition.input: self.place_item(definition.item_id, definition.zone_id, action_key)
self.place_item(definition.item_id, definition.zone_id, action_key) self.wait_until_html_in(definition.feedback_positive, feedback_popup_content)
self.wait_until_html_in(definition.feedback_positive, feedback_popup_content) self.assertEqual(popup.get_attribute('class'), 'popup')
self.assertEqual(popup.get_attribute('class'), 'popup') self.assert_placed_item(definition.item_id, definition.zone_title)
self.assert_placed_item(definition.item_id, definition.zone_title)
def parameterized_item_positive_feedback_on_good_input(self, items_map, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
if definition.input:
self.place_item(definition.item_id, definition.zone_id, action_key)
self.send_input(definition.item_id, definition.input)
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)
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('correct', input_div)
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):
popup = self._get_popup() popup = self._get_popup()
...@@ -224,23 +190,6 @@ class InteractionTestBase(object): ...@@ -224,23 +190,6 @@ class InteractionTestBase(object):
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect') self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
self.assert_reverted_item(definition.item_id) self.assert_reverted_item(definition.item_id)
def parameterized_item_negative_feedback_on_bad_input(self, items_map, scroll_down=100, action_key=None):
popup = self._get_popup()
feedback_popup_content = self._get_popup_content()
# Scroll drop zones into view to make sure Selenium can successfully drop items
self.scroll_down(pixels=scroll_down)
for definition in self._get_items_with_zone(items_map).values():
if definition.input:
self.place_item(definition.item_id, definition.zone_id, action_key)
self.send_input(definition.item_id, '1999999')
self.wait_until_html_in(definition.feedback_negative, feedback_popup_content)
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
self.assert_placed_item(definition.item_id, definition.zone_title)
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('incorrect', input_div)
def parameterized_final_feedback_and_reset(self, items_map, feedback, scroll_down=100, action_key=None): def parameterized_final_feedback_and_reset(self, items_map, feedback, scroll_down=100, action_key=None):
feedback_message = self._get_feedback_message() feedback_message = self._get_feedback_message()
self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check
...@@ -257,10 +206,6 @@ class InteractionTestBase(object): ...@@ -257,10 +206,6 @@ class InteractionTestBase(object):
for item_key, definition in items.items(): 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_id, action_key)
if definition.input:
self.send_input(item_key, definition.input)
input_div = self._get_input_div_by_value(item_key)
self.wait_until_has_class('correct', input_div)
self.assert_placed_item(definition.item_id, definition.zone_title) self.assert_placed_item(definition.item_id, definition.zone_title)
self.wait_until_html_in(feedback['final'], self._get_feedback_message()) self.wait_until_html_in(feedback['final'], self._get_feedback_message())
...@@ -367,15 +312,9 @@ class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase): ...@@ -367,15 +312,9 @@ class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase):
def test_item_positive_feedback_on_good_move(self): def test_item_positive_feedback_on_good_move(self):
self.parameterized_item_positive_feedback_on_good_move(self.items_map) self.parameterized_item_positive_feedback_on_good_move(self.items_map)
def test_item_positive_feedback_on_good_input(self):
self.parameterized_item_positive_feedback_on_good_input(self.items_map)
def test_item_negative_feedback_on_bad_move(self): def test_item_negative_feedback_on_bad_move(self):
self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones) self.parameterized_item_negative_feedback_on_bad_move(self.items_map, self.all_zones)
def test_item_negative_feedback_on_bad_input(self):
self.parameterized_item_negative_feedback_on_bad_input(self.items_map)
def test_final_feedback_and_reset(self): def test_final_feedback_and_reset(self):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback) self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
...@@ -405,9 +344,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration ...@@ -405,9 +344,7 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
{ {
'name': 'edx.drag_and_drop_v2.item.dropped', 'name': 'edx.drag_and_drop_v2.item.dropped',
'data': { 'data': {
'input': None,
'is_correct': True, 'is_correct': True,
'is_correct_location': True,
'item_id': 0, 'item_id': 0,
'location': TOP_ZONE_TITLE, 'location': TOP_ZONE_TITLE,
'location_id': TOP_ZONE_ID, 'location_id': TOP_ZONE_ID,
...@@ -459,18 +396,10 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest): ...@@ -459,18 +396,10 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
self.parameterized_item_positive_feedback_on_good_move(self.items_map, action_key=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') @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_positive_feedback_on_good_input_with_keyboard(self, action_key):
self.parameterized_item_positive_feedback_on_good_input(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): 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) 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') @data(Keys.RETURN, Keys.SPACE, Keys.CONTROL+'m', Keys.COMMAND+'m')
def test_item_negative_feedback_on_bad_input_with_keyboard(self, action_key):
self.parameterized_item_negative_feedback_on_bad_input(self.items_map, 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): 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) self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key)
...@@ -481,7 +410,7 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest): ...@@ -481,7 +410,7 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest): class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'zone-1', "Zone 1", "Yes 1", "No 1"), 0: ItemDefinition(0, 'zone-1', "Zone 1", "Yes 1", "No 1"),
1: ItemDefinition(1, 'zone-2', "Zone 2", "Yes 2", "No 2", "102"), 1: ItemDefinition(1, 'zone-2', "Zone 2", "Yes 2", "No 2"),
2: ItemDefinition(2, None, None, "", "No Zone for this") 2: ItemDefinition(2, None, None, "", "No Zone for this")
} }
...@@ -499,7 +428,7 @@ class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest): ...@@ -499,7 +428,7 @@ class CustomDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest): class CustomHtmlDataInteractionTest(BasicInteractionTest, BaseIntegrationTest):
items_map = { items_map = {
0: ItemDefinition(0, 'zone-1', 'Zone <i>1</i>', "Yes <b>1</b>", "No <b>1</b>"), 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>", "95"), 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>") 2: ItemDefinition(2, None, None, "", "No Zone for <i>X</i>")
} }
...@@ -524,12 +453,12 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): ...@@ -524,12 +453,12 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
item_maps = { item_maps = {
'block1': { 'block1': {
0: ItemDefinition(0, 'zone-1', 'Zone 1', "Yes 1", "No 1"), 0: ItemDefinition(0, 'zone-1', 'Zone 1', "Yes 1", "No 1"),
1: ItemDefinition(1, 'zone-2', 'Zone 2', "Yes 2", "No 2", "102"), 1: ItemDefinition(1, 'zone-2', 'Zone 2', "Yes 2", "No 2"),
2: ItemDefinition(2, None, None, "", "No Zone for this") 2: ItemDefinition(2, None, None, "", "No Zone for this")
}, },
'block2': { 'block2': {
10: ItemDefinition(10, 'zone-51', 'Zone 51', "Correct 1", "Incorrect 1"), 10: ItemDefinition(10, 'zone-51', 'Zone 51', "Correct 1", "Incorrect 1"),
20: ItemDefinition(20, 'zone-52', 'Zone 52', "Correct 2", "Incorrect 2", "102"), 20: ItemDefinition(20, 'zone-52', 'Zone 52', "Correct 2", "Incorrect 2"),
30: ItemDefinition(30, None, None, "", "No Zone for this") 30: ItemDefinition(30, None, None, "", "No Zone for this")
}, },
} }
...@@ -558,12 +487,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): ...@@ -558,12 +487,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
self._switch_to_block(1) self._switch_to_block(1)
self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block2'], scroll_down=900) self.parameterized_item_positive_feedback_on_good_move(self.item_maps['block2'], scroll_down=900)
def test_item_positive_feedback_on_good_input(self):
self._switch_to_block(0)
self.parameterized_item_positive_feedback_on_good_input(self.item_maps['block1'])
self._switch_to_block(1)
self.parameterized_item_positive_feedback_on_good_input(self.item_maps['block2'], scroll_down=900)
def test_item_negative_feedback_on_bad_move(self): def test_item_negative_feedback_on_bad_move(self):
self._switch_to_block(0) self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_move(self.item_maps['block1'], self.all_zones['block1']) self.parameterized_item_negative_feedback_on_bad_move(self.item_maps['block1'], self.all_zones['block1'])
...@@ -572,12 +495,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest): ...@@ -572,12 +495,6 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
self.item_maps['block2'], self.all_zones['block2'], scroll_down=900 self.item_maps['block2'], self.all_zones['block2'], scroll_down=900
) )
def test_item_negative_feedback_on_bad_input(self):
self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_input(self.item_maps['block1'])
self._switch_to_block(1)
self.parameterized_item_negative_feedback_on_bad_input(self.item_maps['block2'], scroll_down=900)
def test_final_feedback_and_reset(self): def test_final_feedback_and_reset(self):
self._switch_to_block(0) self._switch_to_block(0)
self.parameterized_final_feedback_and_reset(self.item_maps['block1'], self.feedback['block1']) self.parameterized_final_feedback_and_reset(self.item_maps['block1'], self.feedback['block1'])
......
...@@ -36,29 +36,25 @@ ...@@ -36,29 +36,25 @@
"displayName": "<b>1</b>", "displayName": "<b>1</b>",
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 0, "id": 0
"inputOptions": false
}, },
{ {
"displayName": "<i>2</i>", "displayName": "<i>2</i>",
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 1, "id": 1
"inputOptions": true
}, },
{ {
"displayName": "X", "displayName": "X",
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 2, "id": 2
"inputOptions": false
}, },
{ {
"displayName": "", "displayName": "",
"imageURL": "http://placehold.it/100x300", "imageURL": "http://placehold.it/100x300",
"expandedImageURL": "http://placehold.it/100x300", "expandedImageURL": "http://placehold.it/100x300",
"id": 3, "id": 3
"inputOptions": false
} }
] ]
} }
...@@ -39,11 +39,7 @@ ...@@ -39,11 +39,7 @@
}, },
"zone": "Zone <b>2</b>", "zone": "Zone <b>2</b>",
"imageURL": "", "imageURL": "",
"id": 1, "id": 1
"inputOptions": {
"value": 100,
"margin": 5
}
}, },
{ {
"displayName": "X", "displayName": "X",
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 0, "id": 0,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"} "size": {"height": "auto", "width": "190px"}
}, },
{ {
...@@ -45,7 +44,6 @@ ...@@ -45,7 +44,6 @@
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 1, "id": 1,
"inputOptions": true,
"size": {"height": "auto", "width": "190px"} "size": {"height": "auto", "width": "190px"}
}, },
{ {
...@@ -53,7 +51,6 @@ ...@@ -53,7 +51,6 @@
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 2, "id": 2,
"inputOptions": false,
"size": {"height": "100px", "width": "100px"} "size": {"height": "100px", "width": "100px"}
}, },
{ {
...@@ -61,7 +58,6 @@ ...@@ -61,7 +58,6 @@
"imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", "imageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"expandedImageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg", "expandedImageURL": "http://i1.kym-cdn.com/entries/icons/square/000/006/151/tumblr_lltzgnHi5F1qzib3wo1_400.jpg",
"id": 3, "id": 3,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"} "size": {"height": "auto", "width": "190px"}
} }
] ]
......
...@@ -46,10 +46,6 @@ ...@@ -46,10 +46,6 @@
"size": { "size": {
"width": "190px", "width": "190px",
"height": "auto" "height": "auto"
},
"inputOptions": {
"value": 100,
"margin": 5
} }
}, },
{ {
......
...@@ -36,29 +36,25 @@ ...@@ -36,29 +36,25 @@
"displayName": "1", "displayName": "1",
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 0, "id": 0
"inputOptions": false
}, },
{ {
"displayName": "2", "displayName": "2",
"imageURL": "", "imageURL": "",
"expandedImageURL": "", "expandedImageURL": "",
"id": 1, "id": 1
"inputOptions": true
}, },
{ {
"displayName": "X", "displayName": "X",
"imageURL": "/static/test_url_expansion", "imageURL": "/static/test_url_expansion",
"expandedImageURL": "/course/test-course/assets/test_url_expansion", "expandedImageURL": "/course/test-course/assets/test_url_expansion",
"id": 2, "id": 2
"inputOptions": false
}, },
{ {
"displayName": "", "displayName": "",
"imageURL": "http://placehold.it/200x100", "imageURL": "http://placehold.it/200x100",
"expandedImageURL": "http://placehold.it/200x100", "expandedImageURL": "http://placehold.it/200x100",
"id": 3, "id": 3
"inputOptions": false
} }
] ]
} }
...@@ -38,11 +38,7 @@ ...@@ -38,11 +38,7 @@
}, },
"zone": "zone-2", "zone": "zone-2",
"imageURL": "", "imageURL": "",
"id": 1, "id": 1
"inputOptions": {
"value": 100,
"margin": 5
}
}, },
{ {
"displayName": "X", "displayName": "X",
......
...@@ -65,7 +65,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -65,7 +65,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None, "overall_feedback": None,
"finished": False, "finished": False,
"correct": False, "correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"] "feedback": self.FEEDBACK[item_id]["incorrect"]
}) })
...@@ -77,7 +76,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -77,7 +76,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None, "overall_feedback": None,
"finished": False, "finished": False,
"correct": False, "correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"] "feedback": self.FEEDBACK[item_id]["incorrect"]
}) })
...@@ -89,79 +87,9 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -89,79 +87,9 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None, "overall_feedback": None,
"finished": False, "finished": False,
"correct": True, "correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[item_id]["correct"] "feedback": self.FEEDBACK[item_id]["correct"]
}) })
def test_do_attempt_with_input(self):
# Drop item that requires numerical input
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "0%", "y_percent": "85%"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": None,
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit incorrect value
data = {"val": 1, "input": "250"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": False,
"correct_location": True,
"feedback": self.FEEDBACK[1]['incorrect'],
"overall_feedback": None
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": False, "zone": self.ZONE_2,
"input": "250",
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
# Submit correct value
data = {"val": 1, "input": "103"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"finished": False,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]['correct'],
"overall_feedback": None,
})
expected_state = {
'items': {
"1": {
"x_percent": "0%", "y_percent": "85%", "correct_input": True, "zone": self.ZONE_2,
"input": "103",
},
},
'finished': False,
'overall_feedback': self.initial_feedback(),
}
self.assertEqual(expected_state, self.call_handler('get_user_state', method="GET"))
def test_grading(self): def test_grading(self):
published_grades = [] published_grades = []
...@@ -182,11 +110,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -182,11 +110,6 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
}) })
self.assertEqual(2, len(published_grades)) self.assertEqual(2, len(published_grades))
self.assertEqual({'value': 0.5, 'max_value': 1}, published_grades[-1])
self.call_handler('do_attempt', {"val": 1, "input": "99"})
self.assertEqual(3, len(published_grades))
self.assertEqual({'value': 1, 'max_value': 1}, published_grades[-1]) self.assertEqual({'value': 1, 'max_value': 1}, published_grades[-1])
def test_do_attempt_final(self): def test_do_attempt_final(self):
...@@ -195,7 +118,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -195,7 +118,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = { expected_state = {
"items": { "items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1} "0": {"x_percent": "33%", "y_percent": "11%", "correct": True, "zone": self.ZONE_1}
}, },
"finished": False, "finished": False,
'overall_feedback': self.initial_feedback(), 'overall_feedback': self.initial_feedback(),
...@@ -204,24 +127,20 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin): ...@@ -204,24 +127,20 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%"} data = {"val": 1, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%"}
res = self.call_handler('do_attempt', data) res = self.call_handler('do_attempt', data)
data = {"val": 1, "input": "99"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, { self.assertEqual(res, {
"overall_feedback": self.FINAL_FEEDBACK, "overall_feedback": self.FINAL_FEEDBACK,
"finished": True, "finished": True,
"correct": True, "correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]["correct"] "feedback": self.FEEDBACK[1]["correct"]
}) })
expected_state = { expected_state = {
"items": { "items": {
"0": { "0": {
"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1, "x_percent": "33%", "y_percent": "11%", "correct": True, "zone": self.ZONE_1,
}, },
"1": { "1": {
"x_percent": "22%", "y_percent": "22%", "correct_input": True, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%", "correct": True, "zone": self.ZONE_2,
"input": "99",
} }
}, },
"finished": True, "finished": True,
......
import unittest import unittest
from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock
from drag_and_drop_v2.default_data import ( from drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID, TARGET_IMG_DESCRIPTION, TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID,
START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
...@@ -45,7 +46,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -45,7 +46,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(zones, DEFAULT_DATA["zones"]) self.assertEqual(zones, DEFAULT_DATA["zones"])
# Items should contain no answer data: # Items should contain no answer data:
self.assertEqual(items, [ self.assertEqual(items, [
{"id": i, "displayName": display_name, "imageURL": "", "expandedImageURL": "", "inputOptions": False} {"id": i, "displayName": display_name, "imageURL": "", "expandedImageURL": ""}
for i, display_name in enumerate( 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", "I don't belong anywhere"]
) )
...@@ -75,15 +76,15 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -75,15 +76,15 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
# Check the result: # Check the result:
self.assertTrue(self.block.completed) self.assertTrue(self.block.completed)
self.assertEqual(self.block.item_state, { self.assertEqual(self.block.item_state, {
'0': {'x_percent': '33%', 'y_percent': '11%', 'zone': TOP_ZONE_ID}, '0': {'x_percent': '33%', 'y_percent': '11%', 'correct': True, 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'zone': MIDDLE_ZONE_ID}, '1': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'zone': BOTTOM_ZONE_ID}, '2': {'x_percent': '99%', 'y_percent': '95%', 'correct': True, 'zone': BOTTOM_ZONE_ID},
}) })
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, 'zone': TOP_ZONE_ID}, '0': {'x_percent': '33%', 'y_percent': '11%', 'correct': True, 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True, 'zone': MIDDLE_ZONE_ID}, '1': {'x_percent': '67%', 'y_percent': '80%', 'correct': True, 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True, 'zone': BOTTOM_ZONE_ID}, '2': {'x_percent': '99%', 'y_percent': '95%', 'correct': True, 'zone': BOTTOM_ZONE_ID},
}, },
'finished': True, 'finished': True,
'overall_feedback': FINISH_FEEDBACK, 'overall_feedback': FINISH_FEEDBACK,
...@@ -97,6 +98,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -97,6 +98,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
def test_studio_submit(self): def test_studio_submit(self):
body = { body = {
'display_name': "Test Drag & Drop", 'display_name': "Test Drag & Drop",
'mode': DragAndDropBlock.ASSESSMENT_MODE,
'show_title': False, 'show_title': False,
'problem_text': "Problem Drag & Drop", 'problem_text': "Problem Drag & Drop",
'show_problem_header': False, 'show_problem_header': False,
...@@ -111,6 +113,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase): ...@@ -111,6 +113,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(res, {'result': 'success'}) self.assertEqual(res, {'result': 'success'})
self.assertEqual(self.block.show_title, False) self.assertEqual(self.block.show_title, False)
self.assertEqual(self.block.mode, DragAndDropBlock.ASSESSMENT_MODE)
self.assertEqual(self.block.display_name, "Test Drag & Drop") self.assertEqual(self.block.display_name, "Test Drag & Drop")
self.assertEqual(self.block.question_text, "Problem Drag & Drop") self.assertEqual(self.block.question_text, "Problem Drag & Drop")
self.assertEqual(self.block.show_question_header, False) self.assertEqual(self.block.show_question_header, False)
......
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