Commit 209bb4a9 by Eugeny Kolpakov Committed by GitHub

Revert "Mode field"

parent 7c2f0846
......@@ -13,6 +13,7 @@ The editor is fully guided. Features include:
* custom text and background colors for items
* optional auto-alignment for items (left, right, center)
* image items
* items prompting for additional (numerical) input after being dropped
* decoy items that don't have a zone
* feedback popups for both correct and incorrect attempts
* introductory and final feedback
......@@ -23,12 +24,12 @@ refreshes. All checking and record keeping is done on the server side.
The following screenshot shows the Drag and Drop XBlock rendered
inside the edX LMS before the user starts solving the problem:
![Student view start](/doc/img/student-view-start.png)
![Student view start](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-start.png)
This screenshot shows the XBlock after the learner successfully
completed the Drag and Drop problem:
![Student view finish](/doc/img/student-view-finish.png)
![Student view finish](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/student-view-finish.png)
Installation
------------
......@@ -94,15 +95,15 @@ Usage
The Drag and Drop XBlock features an interactive editor. Add the Drag
and Drop component to a lesson, then click the `EDIT` button.
![Edit view](/doc/img/edit-view.png)
![Edit view](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view.png)
In the first step, you can set some basic properties of the component,
such as the title, problem mode (Standard vs. Assessment), the maximum score,
the problem text to render above the background image, the introductory feedback (shown
such as the title, the maximum score, the problem text to render
above the background image, the introductory feedback (shown
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](/doc/img/edit-view-zones.png)
![Drop zone edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/ebd0b52d971bbf93b9c3873f310bd72d336d865b/doc/img/edit-view-zones.png)
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
......@@ -124,7 +125,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
potentially, overlap the zones below.
![Drag item edit](/doc/img/edit-view-items.png)
![Drag item edit](https://raw.githubusercontent.com/edx-solutions/xblock-drag-and-drop-v2/c955a38dc3a1aaf609c586d293ce19b282e11ffd/doc/img/edit-view-items.png)
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
......@@ -134,7 +135,16 @@ 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
feedback is shown when dropping the item on an incorrect drop zone.
![Zone dropdown](/doc/img/edit-view-zone-dropdown.png)
Additionally, items can have a numerical value (and an optional error
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
includes all drop zones defined in the previous step and a `none`
......@@ -143,13 +153,6 @@ any zone.
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
----------------
......@@ -258,7 +261,9 @@ Example ("common" fields that are not interesting in this context have been left
{
...
"event": {
"is_correct": true, -- Whether the draggable item has been placed in the correct location.
"input": null,
"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.
"location": "The Top Zone", -- Name of the location the item was dragged to.
},
......@@ -279,9 +284,11 @@ 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",
"accept_language": "en;q=1.0, en;q=0.5",
"event": {
"is_correct_location": true,
"is_correct": true,
"location": "The Top Zone",
"item_id": 0,
"input": null
},
"event_source": "server",
"context": {
......
doc/img/edit-view.png

32.2 KB | W: | H:

doc/img/edit-view.png

27.5 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,9 +32,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
"""
XBlock that implements a friendly Drag-and-Drop problem
"""
STANDARD_MODE = "standard"
ASSESSMENT_MODE = "assessment"
display_name = String(
display_name=_("Title"),
help=_("The title of the drag and drop problem. The title is displayed to learners."),
......@@ -42,20 +39,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
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(
display_name=_("Show title"),
help=_("Display the title to the learner?"),
......@@ -165,6 +148,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
for item in items:
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
# Fall back on "backgroundImage" to be backward-compatible.
image_url = item.get('imageURL') or item.get('backgroundImage')
if image_url:
......@@ -202,14 +186,9 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
field_name: self.ugettext(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 = {
'js_templates': js_templates,
'help_texts': help_texts,
'field_values': field_values,
'self': self,
'data': urllib.quote(json.dumps(self.data)),
}
......@@ -242,7 +221,6 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
@XBlock.json_handler
def studio_submit(self, submissions, suffix=''):
self.display_name = submissions['display_name']
self.mode = submissions['mode']
self.show_title = submissions['show_title']
self.question_text = submissions['problem_text']
self.show_question_header = submissions['show_problem_header']
......@@ -264,13 +242,31 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
feedback = item['feedback']['incorrect']
overall_feedback = None
is_correct = False
if item['zone'] == attempt['zone']: # Student placed item in correct zone
is_correct = True
feedback = item['feedback']['correct']
is_correct_location = False
if 'input' in attempt: # Student submitted numerical value for item
state = self._get_item_state().get(str(item['id']))
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 = {
'zone': attempt['zone'],
'correct': True,
'x_percent': attempt['x_percent'],
'y_percent': attempt['y_percent'],
}
......@@ -304,11 +300,14 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
'item_id': item['id'],
'location': zone.get("title"),
'location_id': zone.get("uid"),
'input': attempt.get('input'),
'is_correct_location': is_correct_location,
'is_correct': is_correct,
})
return {
'correct': is_correct,
'correct_location': is_correct_location,
'finished': self._is_finished(),
'overall_feedback': overall_feedback,
'feedback': feedback
......@@ -372,6 +371,7 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
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
# (because problem was completed before a11y enhancements were implemented),
# deduce zone in which item is placed from definition:
......@@ -443,7 +443,8 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
correct_count += 1
if self._is_correct_input(item, item_state[item_id].get('input')):
correct_count += 1
return correct_count / float(total_count) * self.weight
......@@ -460,7 +461,11 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
completed_count += 1
if 'inputOptions' in item:
if 'input' in item_state[item_id]:
completed_count += 1
else:
completed_count += 1
return completed_count == total_count
......@@ -483,6 +488,25 @@ class DragAndDropBlock(XBlock, XBlockWithSettingsMixin, ThemableXBlockMixin):
return usage_id
@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():
"""
A canned scenario for display in the workbench.
......
......@@ -193,6 +193,49 @@
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 {
opacity: 0.80;
}
......
......@@ -196,6 +196,12 @@
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 {
width: 97%;
margin: 0 1%;
......
......@@ -4,6 +4,20 @@ function DragNDropTemplates(url_name) {
// 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; }; }
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) {
if (!xhr_active) {
return null;
......@@ -22,6 +36,22 @@ 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) {
for (var i = 0; i < ctx.zones.length; i++) {
if (ctx.zones[i].uid === zoneUID) {
......@@ -68,6 +98,10 @@ function DragNDropTemplates(url_name) {
}
} else {
// 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.
var maxWidth = (item.widthPercent || 30) / 100;
var widthPercent = zone.width_percent / 100;
......@@ -98,7 +132,8 @@ function DragNDropTemplates(url_name) {
}
// Define children
var children = [
itemSpinnerTemplate(item.xhr_active)
itemSpinnerTemplate(item.xhr_active),
itemInputTemplate(item.input)
];
var item_content_html = item.displayName;
if (item.imageURL) {
......@@ -351,6 +386,7 @@ function DragAndDropBlock(runtime, element, configuration) {
$element.on('keydown', '.reset-button', function(evt) {
runOnKey(evt, RET, resetProblem);
});
$element.on('click', '.submit-input', submitInput);
// 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.
......@@ -747,9 +783,9 @@ function DragAndDropBlock(runtime, element, configuration) {
$.post(url, JSON.stringify(data), 'json')
.done(function(data){
state.last_action_correct = data.correct;
if (data.correct) {
state.items[item_id].correct = true;
state.last_action_correct = data.correct_location;
if (data.correct_location) {
state.items[item_id].correct_input = Boolean(data.correct);
state.items[item_id].submitting_location = false;
} else {
delete state.items[item_id];
......@@ -767,6 +803,42 @@ 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) {
if (!state.feedback) {
return;
......@@ -775,8 +847,9 @@ function DragAndDropBlock(runtime, element, configuration) {
var target = $(evt.target);
var popup_box = '.xblock--drag-and-drop .popup';
var close_button = '.xblock--drag-and-drop .popup .close';
var submit_input_button = '.xblock--drag-and-drop .submit-input';
if (target.is(popup_box)) {
if (target.is(popup_box) || target.is(submit_input_button)) {
return;
}
if (target.parents(popup_box).length && !target.is(close_button)) {
......@@ -811,17 +884,31 @@ function DragAndDropBlock(runtime, element, configuration) {
var render = function() {
var items = configuration.items.map(function(item) {
var input = null;
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;
if (item.grabbed !== undefined) {
grabbed = item.grabbed;
}
var placed = item_user_state && item_user_state.correct;
var placed = item_user_state && ('input' in item_user_state || item_user_state.correct_input);
var itemProperties = {
value: item.id,
drag_disabled: Boolean(item_user_state || state.finished),
class_name: placed || state.finished ? 'fade' : undefined,
xhr_active: (item_user_state && item_user_state.submitting_location),
input: input,
displayName: item.displayName,
imageURL: item.expandedImageURL,
imageDescription: item.imageDescription,
......
......@@ -414,6 +414,10 @@ function DragAndDropEditBlock(runtime, element, params) {
// 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'
}
if (itemData.inputOptions) {
ctx.numericalValue = itemData.inputOptions.value;
ctx.numericalMargin = itemData.inputOptions.margin;
}
}
ctx.dropdown = _fn.build.form.createDropdown(ctx.zone);
......@@ -482,6 +486,15 @@ function DragAndDropEditBlock(runtime, element, params) {
var widthPercent = $el.find('.item-width').val();
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);
}
});
......@@ -491,7 +504,6 @@ function DragAndDropEditBlock(runtime, element, params) {
var data = {
'display_name': $element.find('#display-name').val(),
'mode': $element.find("#problem-mode").val(),
'show_title': $element.find('.show-title').is(':checked'),
'weight': $element.find('#weight').val(),
'problem_text': $element.find('#problem-text').val(),
......
......@@ -32,6 +32,16 @@
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 {
opacity: 0.5;
}
......@@ -75,4 +85,4 @@
.themed-xblock.xblock--drag-and-drop .link-button {
cursor: pointer;
color: #3384ca;
}
}
\ No newline at end of file
......@@ -21,16 +21,6 @@
<span class="sr">{{ help_texts.show_title }}</span>
</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>
<input id="weight" type="number" step="0.1" value="{{ self.weight|unlocalize }}" />
......
......@@ -131,5 +131,23 @@
<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" />%
</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>
</script>
......@@ -248,6 +248,19 @@ msgid ""
"automatic width):"
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
msgid "Loading drag and drop problem."
msgstr ""
......
......@@ -304,6 +304,29 @@ 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 ""
"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
msgid "Loading drag and drop problem."
msgstr "Löädïng dräg änd dröp prößlém. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢т#"
......
......@@ -20,4 +20,4 @@ disable=
no-member
[SIMILARITIES]
min-similarity-lines=4
min-similarity-lines=8
......@@ -47,6 +47,10 @@
"size": {
"width": "190px",
"height": "100px"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
......@@ -73,4 +77,4 @@
"finish": "Final Feedback"
},
"title": "Drag and Drop (Old-style data)"
}
}
\ No newline at end of file
......@@ -36,7 +36,11 @@
},
"zone": "zone-2",
"imageURL": "",
"id": 1
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
......
......@@ -36,7 +36,11 @@
},
"zone": "zone-52",
"imageURL": "",
"id": 20
"id": 20,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
......
......@@ -36,7 +36,11 @@
},
"zone": "zone-2",
"imageURL": "",
"id": 1
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "<span style='color:red'>X</span>",
......
......@@ -22,6 +22,7 @@ class TestCustomDataDragAndDropRendering(BaseIntegrationTest):
self.assertEqual(len(items), 3)
self.assertIn('<b>1</b>', self.get_element_html(items[0]))
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]))
def test_background_image(self):
......
......@@ -27,12 +27,13 @@ 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_id, zone_title, feedback_positive, feedback_negative, input_value=None):
self.feedback_negative = feedback_negative
self.feedback_positive = feedback_positive
self.zone_id = zone_id
self.zone_title = zone_title
self.item_id = item_id
self.input = input_value
class InteractionTestBase(object):
......@@ -76,6 +77,10 @@ class InteractionTestBase(object):
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]
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
dialog_modal_overlay = dialog.find_element_by_css_selector('.modal-window-overlay')
dialog_modal = dialog.find_element_by_css_selector('.modal-window')
......@@ -114,6 +119,17 @@ class InteractionTestBase(object):
self._page.send_keys(Keys.TAB)
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):
self.assertEqual(item.get_attribute('aria-grabbed'), 'true')
......@@ -169,10 +185,28 @@ 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.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)
if not definition.input:
self.place_item(definition.item_id, definition.zone_id, 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)
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):
popup = self._get_popup()
......@@ -190,6 +224,23 @@ class InteractionTestBase(object):
self.assertEqual(popup.get_attribute('class'), 'popup popup-incorrect')
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):
feedback_message = self._get_feedback_message()
self.assertEqual(self.get_element_html(feedback_message), feedback['intro']) # precondition check
......@@ -206,6 +257,10 @@ class InteractionTestBase(object):
for item_key, definition in items.items():
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.wait_until_html_in(feedback['final'], self._get_feedback_message())
......@@ -312,9 +367,15 @@ class BasicInteractionTest(DefaultDataTestMixin, InteractionTestBase):
def test_item_positive_feedback_on_good_move(self):
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):
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):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback)
......@@ -344,7 +405,9 @@ class EventsFiredTest(DefaultDataTestMixin, InteractionTestBase, BaseIntegration
{
'name': 'edx.drag_and_drop_v2.item.dropped',
'data': {
'input': None,
'is_correct': True,
'is_correct_location': True,
'item_id': 0,
'location': TOP_ZONE_TITLE,
'location_id': TOP_ZONE_ID,
......@@ -396,10 +459,18 @@ class KeyboardInteractionTest(BasicInteractionTest, BaseIntegrationTest):
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')
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):
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')
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):
self.parameterized_final_feedback_and_reset(self.items_map, self.feedback, action_key=action_key)
......@@ -410,7 +481,7 @@ 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"),
1: ItemDefinition(1, 'zone-2', "Zone 2", "Yes 2", "No 2", "102"),
2: ItemDefinition(2, None, None, "", "No Zone for this")
}
......@@ -428,7 +499,7 @@ 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>"),
1: ItemDefinition(1, 'zone-2', 'Zone <b>2</b>', "Yes <i>2</i>", "No <i>2</i>", "95"),
2: ItemDefinition(2, None, None, "", "No Zone for <i>X</i>")
}
......@@ -453,12 +524,12 @@ 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"),
1: ItemDefinition(1, 'zone-2', 'Zone 2', "Yes 2", "No 2", "102"),
2: ItemDefinition(2, None, 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"),
20: ItemDefinition(20, 'zone-52', 'Zone 52', "Correct 2", "Incorrect 2", "102"),
30: ItemDefinition(30, None, None, "", "No Zone for this")
},
}
......@@ -487,6 +558,12 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
self._switch_to_block(1)
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):
self._switch_to_block(0)
self.parameterized_item_negative_feedback_on_bad_move(self.item_maps['block1'], self.all_zones['block1'])
......@@ -495,6 +572,12 @@ class MultipleBlocksDataInteraction(InteractionTestBase, BaseIntegrationTest):
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):
self._switch_to_block(0)
self.parameterized_final_feedback_and_reset(self.item_maps['block1'], self.feedback['block1'])
......
......@@ -36,25 +36,29 @@
"displayName": "<b>1</b>",
"imageURL": "",
"expandedImageURL": "",
"id": 0
"id": 0,
"inputOptions": false
},
{
"displayName": "<i>2</i>",
"imageURL": "",
"expandedImageURL": "",
"id": 1
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "",
"expandedImageURL": "",
"id": 2
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "http://placehold.it/100x300",
"expandedImageURL": "http://placehold.it/100x300",
"id": 3
"id": 3,
"inputOptions": false
}
]
}
......@@ -39,7 +39,11 @@
},
"zone": "Zone <b>2</b>",
"imageURL": "",
"id": 1
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
......
......@@ -37,6 +37,7 @@
"imageURL": "",
"expandedImageURL": "",
"id": 0,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
},
{
......@@ -44,6 +45,7 @@
"imageURL": "",
"expandedImageURL": "",
"id": 1,
"inputOptions": true,
"size": {"height": "auto", "width": "190px"}
},
{
......@@ -51,6 +53,7 @@
"imageURL": "",
"expandedImageURL": "",
"id": 2,
"inputOptions": false,
"size": {"height": "100px", "width": "100px"}
},
{
......@@ -58,6 +61,7 @@
"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",
"id": 3,
"inputOptions": false,
"size": {"height": "auto", "width": "190px"}
}
]
......
......@@ -46,6 +46,10 @@
"size": {
"width": "190px",
"height": "auto"
},
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
......
......@@ -36,25 +36,29 @@
"displayName": "1",
"imageURL": "",
"expandedImageURL": "",
"id": 0
"id": 0,
"inputOptions": false
},
{
"displayName": "2",
"imageURL": "",
"expandedImageURL": "",
"id": 1
"id": 1,
"inputOptions": true
},
{
"displayName": "X",
"imageURL": "/static/test_url_expansion",
"expandedImageURL": "/course/test-course/assets/test_url_expansion",
"id": 2
"id": 2,
"inputOptions": false
},
{
"displayName": "",
"imageURL": "http://placehold.it/200x100",
"expandedImageURL": "http://placehold.it/200x100",
"id": 3
"id": 3,
"inputOptions": false
}
]
}
......@@ -38,7 +38,11 @@
},
"zone": "zone-2",
"imageURL": "",
"id": 1
"id": 1,
"inputOptions": {
"value": 100,
"margin": 5
}
},
{
"displayName": "X",
......
......@@ -65,6 +65,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
......@@ -76,6 +77,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None,
"finished": False,
"correct": False,
"correct_location": False,
"feedback": self.FEEDBACK[item_id]["incorrect"]
})
......@@ -87,9 +89,79 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
"overall_feedback": None,
"finished": False,
"correct": True,
"correct_location": True,
"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):
published_grades = []
......@@ -110,6 +182,11 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
})
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])
def test_do_attempt_final(self):
......@@ -118,7 +195,7 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
expected_state = {
"items": {
"0": {"x_percent": "33%", "y_percent": "11%", "correct": True, "zone": self.ZONE_1}
"0": {"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1}
},
"finished": False,
'overall_feedback': self.initial_feedback(),
......@@ -127,20 +204,24 @@ class BaseDragAndDropAjaxFixture(TestCaseMixin):
data = {"val": 1, "zone": self.ZONE_2, "x_percent": "22%", "y_percent": "22%"}
res = self.call_handler('do_attempt', data)
data = {"val": 1, "input": "99"}
res = self.call_handler('do_attempt', data)
self.assertEqual(res, {
"overall_feedback": self.FINAL_FEEDBACK,
"finished": True,
"correct": True,
"correct_location": True,
"feedback": self.FEEDBACK[1]["correct"]
})
expected_state = {
"items": {
"0": {
"x_percent": "33%", "y_percent": "11%", "correct": True, "zone": self.ZONE_1,
"x_percent": "33%", "y_percent": "11%", "correct_input": True, "zone": self.ZONE_1,
},
"1": {
"x_percent": "22%", "y_percent": "22%", "correct": True, "zone": self.ZONE_2,
"x_percent": "22%", "y_percent": "22%", "correct_input": True, "zone": self.ZONE_2,
"input": "99",
}
},
"finished": True,
......
import unittest
from drag_and_drop_v2.drag_and_drop_v2 import DragAndDropBlock
from drag_and_drop_v2.default_data import (
TARGET_IMG_DESCRIPTION, TOP_ZONE_ID, MIDDLE_ZONE_ID, BOTTOM_ZONE_ID,
START_FEEDBACK, FINISH_FEEDBACK, DEFAULT_DATA
......@@ -46,7 +45,7 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(zones, DEFAULT_DATA["zones"])
# Items should contain no answer data:
self.assertEqual(items, [
{"id": i, "displayName": display_name, "imageURL": "", "expandedImageURL": ""}
{"id": i, "displayName": display_name, "imageURL": "", "expandedImageURL": "", "inputOptions": False}
for i, display_name in enumerate(
["Goes to the top", "Goes to the middle", "Goes to the bottom", "I don't belong anywhere"]
)
......@@ -76,15 +75,15 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
# Check the result:
self.assertTrue(self.block.completed)
self.assertEqual(self.block.item_state, {
'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},
'0': {'x_percent': '33%', 'y_percent': '11%', 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'zone': BOTTOM_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},
'0': {'x_percent': '33%', 'y_percent': '11%', 'correct_input': True, 'zone': TOP_ZONE_ID},
'1': {'x_percent': '67%', 'y_percent': '80%', 'correct_input': True, 'zone': MIDDLE_ZONE_ID},
'2': {'x_percent': '99%', 'y_percent': '95%', 'correct_input': True, 'zone': BOTTOM_ZONE_ID},
},
'finished': True,
'overall_feedback': FINISH_FEEDBACK,
......@@ -98,7 +97,6 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
def test_studio_submit(self):
body = {
'display_name': "Test Drag & Drop",
'mode': DragAndDropBlock.ASSESSMENT_MODE,
'show_title': False,
'problem_text': "Problem Drag & Drop",
'show_problem_header': False,
......@@ -113,7 +111,6 @@ class BasicTests(TestCaseMixin, unittest.TestCase):
self.assertEqual(res, {'result': 'success'})
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.question_text, "Problem Drag & Drop")
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