Commit dbf85344 by Xavier Antoviaque

Merge pull request #27 from edx-solutions/mtyaka/virtual-dom

Use virtual-dom for rendering the interface
parents d2ac471c d1e4d124
language: python
python:
- "2.7"
before_install:
- "export DISPLAY=:99"
- "sh -e /etc/init.d/xvfb start"
install:
- "sh install_test_deps.sh"
- "pip uninstall -y xblock-drag-and-drop-v2"
- "python setup.py sdist"
- "pip install dist/xblock-drag-and-drop-v2-0.1.tar.gz"
script: pep8 drag_and_drop_v2 --max-line-length=120 && python run_tests.py
notifications:
email: false
\ No newline at end of file
......@@ -93,22 +93,15 @@ You can define an arbitrary number of drag items.
Testing
-------
In a virtualenv, run
Inside a fresh virtualenv, run
```bash
$ cd .../xblock-drag-and-drop-v2/
$ pip install -r tests/requirements.txt
$ sh install_test_deps.sh
```
To run the tests, from the xblock-drag-and-drop-v2 repository root:
```bash
$ tests/manage.py test --rednose
```
To include coverage report (although selenium tends to crash with
segmentation faults when collection test coverage):
```bash
$ tests/manage.py test --rednose --with-cover --cover-package=drag_and_drop_v2
$ python run_tests.py
```
default_data = {
DEFAULT_DATA = {
"zones": [
{
"index": 1,
......
......@@ -3,7 +3,6 @@
# Imports ###########################################################
import logging
import json
import webob
import copy
......@@ -14,12 +13,7 @@ from xblock.fields import Scope, String, Dict, Float, Boolean
from xblock.fragment import Fragment
from .utils import render_template, load_resource
from .default_data import default_data
# Globals ###########################################################
log = logging.getLogger(__name__)
from .default_data import DEFAULT_DATA
# Classes ###########################################################
......@@ -53,7 +47,7 @@ class DragAndDropBlock(XBlock):
display_name="Drag and Drop",
help="JSON spec as generated by the builder",
scope=Scope.content,
default=default_data
default=DEFAULT_DATA
)
item_state = Dict(
......@@ -75,30 +69,22 @@ class DragAndDropBlock(XBlock):
Player view, displayed to the student
"""
js_templates = load_resource('/templates/html/js_templates.html')
context = {
'js_templates': js_templates,
'title': self.display_name,
'question_text': self.question_text,
}
fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop.html', context))
CSS_URLS = (
fragment.add_content(render_template('/templates/html/drag_and_drop.html'))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop.css'
)
JS_URLS = (
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/jquery-ui-touch-punch-0.2.3.min.js', # Makes it work on touch devices
'public/js/vendor/jquery.html5-placeholder-shim.js',
'public/js/vendor/handlebars-v1.1.2.js',
'public/js/vendor/virtual-dom-1.3.0.min.js',
'public/js/drag_and_drop.js',
'public/js/view.js',
)
for css_url in CSS_URLS:
for css_url in css_urls:
fragment.add_css_url(self.runtime.local_resource_url(self, css_url))
for js_url in JS_URLS:
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropBlock')
......@@ -119,18 +105,21 @@ class DragAndDropBlock(XBlock):
fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop_edit.html', context))
fragment.add_css_url(self.runtime.local_resource_url(self,
'public/css/vendor/jquery-ui-1.10.4.custom.min.css'))
fragment.add_css_url(self.runtime.local_resource_url(self,
'public/css/drag_and_drop_edit.css'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/jquery-ui-1.10.4.custom.min.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/jquery.html5-placeholder-shim.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/vendor/handlebars-v1.1.2.js'))
fragment.add_javascript_url(self.runtime.local_resource_url(self,
'public/js/drag_and_drop_edit.js'))
css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop_edit.css'
)
js_urls = (
'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/jquery.html5-placeholder-shim.js',
'public/js/vendor/handlebars-v1.1.2.js',
'public/js/drag_and_drop_edit.js',
)
for css_url in css_urls:
fragment.add_css_url(self.runtime.local_resource_url(self, css_url))
for js_url in js_urls:
fragment.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropEditBlock')
......@@ -149,33 +138,12 @@ class DragAndDropBlock(XBlock):
@XBlock.handler
def get_data(self, request, suffix=''):
data = copy.deepcopy(self.data)
for item in data['items']:
# Strip answers
del item['feedback']
del item['zone']
item['inputOptions'] = item.has_key('inputOptions')
if not self._is_finished():
del data['feedback']['finish']
item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id)
item['correct_input'] = self._is_correct_input(definition, item.get('input'))
data['state'] = {
'items': item_state,
'finished': self._is_finished()
}
data = self._get_data()
return webob.response.Response(body=json.dumps(data))
@XBlock.json_handler
def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val'])
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')
state = None
feedback = item['feedback']['incorrect']
......@@ -195,7 +163,7 @@ class DragAndDropBlock(XBlock):
is_correct = False
elif item['zone'] == attempt['zone']:
is_correct_location = True
if item.has_key('inputOptions'):
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
......@@ -204,7 +172,11 @@ class DragAndDropBlock(XBlock):
# If this item has no input value set, we are done with it.
is_correct = True
feedback = item['feedback']['correct']
state = {'top': attempt['top'], 'left': attempt['left']}
state = {
'top': attempt['top'],
'left': attempt['left'],
'absolute': True # flag for backwards compatibility (values used to be relative)
}
if state:
self.item_state[str(item['id'])] = state
......@@ -247,7 +219,34 @@ class DragAndDropBlock(XBlock):
@XBlock.json_handler
def reset(self, data, suffix=''):
self.item_state = {}
return {'result':'success'}
return self._get_data()
def _get_data(self):
data = copy.deepcopy(self.data)
for item in data['items']:
# Strip answers
del item['feedback']
del item['zone']
item['inputOptions'] = 'inputOptions' in item
if not self._is_finished():
del data['feedback']['finish']
item_state = self._get_item_state()
for item_id, item in item_state.iteritems():
definition = next(i for i in self.data['items'] if str(i['id']) == item_id)
item['correct_input'] = self._is_correct_input(definition, item.get('input'))
data['state'] = {
'items': item_state,
'finished': self._is_finished()
}
data['title'] = self.display_name
data['question_text'] = self.question_text
return data
def _get_item_state(self):
"""
......@@ -264,24 +263,6 @@ class DragAndDropBlock(XBlock):
return state
def _is_correct_input(self, 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:
return False
else:
expected_value = input_options['value']
margin = input_options['margin']
return abs(submitted_value - expected_value) <= margin
else:
return True
def _get_grade(self):
"""
Returns the student's grade for this block.
......@@ -313,8 +294,8 @@ class DragAndDropBlock(XBlock):
total_count += 1
item_id = str(item['id'])
if item_id in item_state:
if item.has_key('inputOptions'):
if item_state[item_id].has_key('input'):
if 'inputOptions' in item:
if 'input' in item_state[item_id]:
completed_count += 1
else:
completed_count += 1
......@@ -325,24 +306,43 @@ class DragAndDropBlock(XBlock):
def publish_event(self, data, suffix=''):
try:
event_type = data.pop('event_type')
except KeyError as e:
except KeyError:
return {'result': 'error', 'message': 'Missing event_type in JSON data'}
data['user_id'] = self.scope_ids.user_id
data['component_id'] = self._get_unique_id()
self.runtime.publish(self, event_type, data)
return {'result':'success'}
return {'result': 'success'}
def _get_unique_id(self):
try:
unique_id = self.location.name
unique_id = self.location.name # pylint: disable=no-member
except AttributeError:
# workaround for xblock workbench
unique_id = self.parent and self.parent.replace('.', '-')
return unique_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.
......
......@@ -47,15 +47,15 @@
width: 210px;
margin: 10px;
padding: 0 !important; /* LMS tries to override this */
font-size: 14px;
position: relative;
display: inline;
float: left;
list-style-type: none;
}
.xblock--drag-and-drop .drag-container .items .option {
.xblock--drag-and-drop .drag-container .option {
width: 190px;
font-size: 14px;
background: #2e83cd;
color: #fff;
position: relative;
......@@ -68,28 +68,23 @@
opacity: 1;
}
.xblock--drag-and-drop .drag-container .items .option img {
.xblock--drag-and-drop .drag-container .option img {
max-width: 100%;
}
.xblock--drag-and-drop .drag-container .items .option .numerical-input {
display: none;
.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 .items .option.within-dropzone .numerical-input {
display: block;
}
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input .input {
.xblock--drag-and-drop .drag-container .option .numerical-input .input {
display: inline-block;
width: 144px;
}
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input .submit-input {
.xblock--drag-and-drop .drag-container .option .numerical-input .submit-input {
box-sizing: border-box;
position: absolute;
left: 150px;
......@@ -97,22 +92,22 @@
height: 24px;
}
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.correct .input-submit,
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.incorrect .input-submit {
.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 .items .option.within-dropzone .numerical-input.correct .input {
.xblock--drag-and-drop .drag-container .option .numerical-input.correct .input {
background: #ceffce;
color: #0dad0d;
}
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.incorrect .input {
.xblock--drag-and-drop .drag-container .option .numerical-input.incorrect .input {
background: #ffcece;
color: #ad0d0d;
}
.xblock--drag-and-drop .drag-container .items .option.fade {
.xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.5;
}
......@@ -129,7 +124,6 @@
}
.xblock--drag-and-drop .target-img {
display: none;
background: url('../img/triangle.png') no-repeat;
width: 100%;
height: 100%;
......@@ -229,5 +223,4 @@
float: right;
color: #3384CA;
margin-top: 3px;
display: none;
}
function DragAndDropBlock(runtime, element) {
function publish_event(data) {
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'publish_event'),
data: JSON.stringify(data)
});
}
var dragAndDrop = (function($) {
var _fn = {
pupup_ts: Date.now(),
// DOM Elements
$ul: $('.xblock--drag-and-drop .items', element),
$target: $('.xblock--drag-and-drop .target-img', element),
$feedback: $('.xblock--drag-and-drop .feedback .message', element),
$popup: $('.xblock--drag-and-drop .popup', element),
$reset_button: $('.xblock--drag-and-drop .reset-button', element),
// Cannot set until items added to DOM
$items: {}, // $('.xblock--drag-and-drop .items .option'),
$zones: {}, // $('.xblock--drag-and-drop .target .zone'),
// jQuery UI Draggable options
options: {
drag: {
var root = $(element).find('.xblock--drag-and-drop')[0];
var __state;
var __vdom = virtualDom.h(); // blank virtual DOM
var init = function() {
$.ajax(runtime.handlerUrl(element, 'get_data'), {
dataType: 'json'
}).done(function(data){
setState(data);
initDroppable();
});
$(document).on('mousedown touchstart', closePopup);
$(element).on('click', '.reset-button', resetExercise);
$(element).on('click', '.submit-input', submitInput);
publishEvent({event_type: 'xblock.drag-and-drop-v2.loaded'});
};
var getState = function() {
return __state;
};
var setState = function(new_state) {
if (new_state.state.feedback) {
if (new_state.state.feedback !== __state.state.feedback) {
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: __state.state.feedback,
manually: false
});
}
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.opened',
content: new_state.state.feedback
});
}
__state = new_state;
updateDOM(new_state);
destroyDraggable();
if (!new_state.state.finished) {
initDraggable();
}
};
var updateDOM = function(state) {
var new_vdom = render(state);
var patches = virtualDom.diff(__vdom, new_vdom);
root = virtualDom.patch(root, patches);
__vdom = new_vdom;
};
var publishEvent = function(data) {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'publish_event'),
data: JSON.stringify(data)
});
};
var initDroppable = function() {
$(root).find('.zone').droppable({
accept: '.xblock--drag-and-drop .items .option',
tolerance: 'pointer',
drop: function(evt, ui) {
var item_id = ui.draggable.data('value');
var zone = $(this).data('zone');
var position = ui.draggable.position();
var top = position.top + 'px';
var left = position.left + 'px';
var state = getState();
state.state.items[item_id] = {
top: top,
left: left,
absolute: true,
submitting_location: true
};
// Wrap in setTimeout to let the droppable event finish.
setTimeout(function() {
setState(state);
submitLocation(item_id, zone, top, left);
}, 0);
}
});
};
var initDraggable = function() {
$(root).find('.items .option').not('[data-drag-disabled=true]').each(function() {
try {
$(this).draggable({
containment: '.xblock--drag-and-drop .drag-container',
cursor: 'move',
stack: '.xblock--drag-and-drop .items .option'
},
drop: {
accept: '.xblock--drag-and-drop .items .option',
tolerance: 'pointer'
}
},
tpl: {
init: function() {
_fn.tpl = {
item: Handlebars.compile($("#item-tpl", element).html()),
imageItem: Handlebars.compile($("#image-item-tpl", element).html()),
zoneElement: Handlebars.compile($("#zone-element-tpl", element).html())
};
}
},
init: function(data) {
_fn.data = data;
// Compile templates
_fn.tpl.init();
// Add the items to the page
_fn.items.draw();
_fn.zones.draw();
// Init drag and drop plugin
_fn.$items.draggable(_fn.options.drag);
_fn.$zones.droppable(_fn.options.drop);
// Init click handlers
_fn.eventHandlers.init(_fn.$items, _fn.$zones);
// Position the already correct items
_fn.items.init();
// Load welcome or final feedback
if (_fn.data.state.finished)
_fn.finish(_fn.data.feedback.finish);
else
_fn.feedback.set(_fn.data.feedback.start);
// Set the target image
if (_fn.data.targetImg)
_fn.$target.css('background', 'url(' + _fn.data.targetImg + ') no-repeat');
// Display target image
_fn.$target.show();
// Display the zone names if required
if (_fn.data.displayLabels) {
$('p', _fn.$zones).css('visibility', 'visible');
}
},
finish: function(final_feedback) {
// Disable any decoy items
_fn.$items.draggable('disable');
_fn.$reset_button.show();
// Show final feedback
if (final_feedback) _fn.feedback.set(final_feedback);
},
reset: function() {
_fn.$items.draggable('enable');
_fn.$items.find('.numerical-input').removeClass('correct incorrect');
_fn.$items.find('.numerical-input .input').prop('disabled', false).val('');
_fn.$items.find('.numerical-input .submit-input').prop('disabled', false);
_fn.$items.each(function(index, element) {
_fn.eventHandlers.drag.reset($(element));
});
_fn.$popup.hide();
_fn.$reset_button.hide();
_fn.feedback.set(_fn.data.feedback.start);
},
eventHandlers: {
init: function($drag, $dropzone) {
var handlers = _fn.eventHandlers;
$drag.on('dragstart', handlers.drag.start);
$drag.on('dragstop', handlers.drag.stop);
$dropzone.on('drop', handlers.drop.success);
$dropzone.on('dropover', handlers.drop.hover);
$(element).on('click', '.submit-input', handlers.drag.submitInput);
$(document).on('click', function(evt) {
// Click should only close the popup if the popup has been
// visible for at least one second.
var popup_timeout = 1000;
if (Date.now() - _fn.popup_ts > popup_timeout) {
handlers.popup.close(evt);
}
});
_fn.$reset_button.on('click', handlers.problem.reset);
},
problem: {
reset: function(event, ui) {
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, "reset"),
data: "{}",
success: _fn.reset
});
}
},
popup: {
close: function(event, ui) {
target = $(event.target);
popup_box = ".xblock--drag-and-drop .popup";
close_button = ".xblock--drag-and-drop .popup .close";
if (target.is(popup_box)) {
return;
};
if (target.parents(popup_box).length>0 && !target.is(close_button)) {
return;
};
_fn.$popup.hide();
publish_event({
event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: _fn.$popup.find(".popup-content").text(),
manually: true
});
}
},
drag: {
start: function(event, ui) {
_fn.eventHandlers.popup.close(event, ui);
target = $(event.currentTarget);
target.removeClass('within-dropzone fade');
var item_id = target.data("value");
publish_event({event_type:'xblock.drag-and-drop-v2.item.picked-up', item_id:item_id});
},
stop: function(event, ui) {
var $el = $(event.currentTarget);
if (!$el.hasClass('within-dropzone')) {
// Return to original position
_fn.eventHandlers.drag.reset($el);
} else {
_fn.eventHandlers.drag.submitLocation($el);
}
},
submitLocation: function($el) {
var val = $el.data('value'),
zone = $el.data('zone') || null;
$.post(runtime.handlerUrl(element, 'do_attempt'),
JSON.stringify({
val: val,
zone: zone,
top: $el.css('top'),
left: $el.css('left')
}), 'json').done(function(data){
if (data.correct_location) {
$el.draggable('disable');
if (data.finished) {
_fn.finish(data.final_feedback);
}
} else {
// Return to original position
_fn.eventHandlers.drag.reset($el);
}
if (data.feedback) {
_fn.feedback.popup(data.feedback, data.correct);
}
stack: '.xblock--drag-and-drop .items .option',
revert: 'invalid',
revertDuration: 150,
start: function(evt, ui) {
var item_id = $(this).data('value');
publishEvent({
event_type: 'xblock.drag-and-drop-v2.item.picked-up',
item_id: item_id
});
},
submitInput: function(evt) {
var $el = $(this).closest('li.option');
var $input_div = $el.find('.numerical-input');
var $input = $input_div.find('.input');
var val = $el.data('value');
if (!$input.val()) {
// Don't submit if the user didn't enter anything yet.
return;
}
$input.prop('disabled', true);
$input_div.find('.submit-input').prop('disabled', true);
$.post(runtime.handlerUrl(element, 'do_attempt'),
JSON.stringify({
val: val,
input: $input.val()
}), 'json').done(function(data){
if (data.correct) {
$input_div.removeClass('incorrect').addClass('correct');
} else {
$input_div.removeClass('correct').addClass('incorrect');
}
if (data.finished) {
_fn.finish(data.final_feedback);
}
if (data.feedback) {
_fn.feedback.popup(data.feedback, data.correct);
}
});
},
set: function($el, top, left) {
$el.addClass('within-dropzone')
.css({
top: top,
left: left
})
.draggable('disable');
},
reset: function($el) {
$el.removeClass('within-dropzone fade')
.css({
top: '',
left: ''
});
}
},
drop: {
hover: function(event, ui) {
var zone = $(event.currentTarget).data('zone');
ui.draggable.data('zone', zone);
},
success: function(event, ui) {
ui.draggable.addClass('within-dropzone');
var item = _fn.data.items[ui.draggable.data('value')];
if (item.inputOptions) {
ui.draggable.find('.input').focus();
}
}
}
},
items: {
init: function() {
_fn.$items.each(function (){
var $el = $(this),
saved_entry = _fn.data.state.items[$el.data('value')];
if (saved_entry) {
var $input_div = $el.find('.numerical-input')
var $input = $input_div.find('.input');
$input.val(saved_entry.input);
console.log('wuwuwu', saved_entry)
if ('input' in saved_entry) {
$input_div.addClass(saved_entry.correct_input ? 'correct' : 'incorrect');
$input.prop('disabled', true);
$input_div.find('.submit-input').prop('disabled', true);
}
if ('input' in saved_entry || saved_entry.correct_input) {
$el.addClass('fade');
}
_fn.eventHandlers.drag.set($el, saved_entry.top, saved_entry.left);
}
});
},
draw: function() {
var list = [],
items = _fn.data.items,
tpl = _fn.tpl.item,
img_tpl = _fn.tpl.imageItem;
items.forEach(function(item) {
if (item.backgroundImage.length > 0) {
list.push(img_tpl(item));
} else {
list.push(tpl(item));
}
});
// Update DOM
_fn.$ul.html(list.join(''));
// Set variable
_fn.$items = $('.xblock--drag-and-drop .items .option', element);
}
},
zones: {
draw: function() {
var html = [],
zones = _fn.data.zones,
tpl = _fn.tpl.zoneElement,
i,
len = zones.length;
for (i=0; i<len; i++) {
html.push(tpl(zones[i]));
}
// Update DOM
_fn.$target.html(html.join(''));
// Set variable
_fn.$zones = _fn.$target.find('.zone');
}
},
feedback: {
// Update DOM with feedback
set: function(str) {
if ($.trim(str) === '') _fn.$feedback.parent().hide();
else _fn.$feedback.parent().show();
return _fn.$feedback.html(str);
},
// Show a feedback popup
popup: function(str, boo) {
if (str === undefined || str === '') return;
if (_fn.$popup.is(":visible")) {
publish_event({
event_type: "xblock.drag-and-drop-v2.feedback.closed",
content: _fn.$popup.find(".popup-content").text(),
manually: false
});
}
publish_event({
event_type: "xblock.drag-and-drop-v2.feedback.opened",
content: str
});
_fn.$popup.find(".popup-content").html(str);
_fn.$popup.show();
_fn.popup_ts = Date.now();
});
} catch (e) {
// Initializing the draggable will fail if draggable was already
// initialized. That's expected, ignore the exception.
}
});
};
var destroyDraggable = function() {
$(root).find('.items .option[data-drag-disabled=true]').each(function() {
try {
$(this).draggable('destroy');
} catch (e) {
// Destroying the draggable will fail if draggable was
// not initialized in the first place. Ignore the exception.
}
});
};
var submitLocation = function(item_id, zone, top, left) {
if (!zone) {
return;
}
var url = runtime.handlerUrl(element, 'do_attempt');
var data = {val: item_id, zone: zone, top: top, left: left};
$.post(url, JSON.stringify(data), 'json').done(function(data){
var state = getState();
if (data.correct_location) {
state.state.items[item_id].correct_input = Boolean(data.correct);
state.state.items[item_id].submitting_location = false;
} else {
delete state.state.items[item_id];
}
state.state.feedback = data.feedback;
if (data.finished) {
state.state.finished = true;
state.feedback.finish = data.final_feedback;
}
setState(state);
});
};
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;
}
var state = getState();
state.state.items[item_id].input = input_value;
state.state.items[item_id].submitting_input = true;
setState(state);
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.state.items[item_id].submitting_input = false;
state.state.items[item_id].correct_input = data.correct;
state.state.feedback = data.feedback;
if (data.finished) {
state.state.finished = true;
state.feedback.finish = data.final_feedback;
}
setState(state);
});
};
var closePopup = function(evt) {
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';
var state = getState();
if (!state.state.feedback) {
return;
}
if (target.is(popup_box) || target.is(submit_input_button)) {
return;
}
if (target.parents(popup_box).length && !target.is(close_button)) {
return;
}
publishEvent({
event_type: 'xblock.drag-and-drop-v2.feedback.closed',
content: state.state.feedback,
manually: true
});
delete state.state.feedback;
setState(state);
};
var resetExercise = function() {
$.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'reset'),
data: '{}',
success: setState
});
};
var render = function(state) {
var items = state.items.map(function(item) {
var item_state = state.state.items[item.id];
var position = item_state || {};
var input = null;
if (item.inputOptions) {
input = {
is_visible: item_state && !item_state.submitting_location,
has_value: Boolean(item_state && 'input' in item_state),
value : (item_state && item_state.input) || '',
class_name: undefined,
};
if (input.has_value && !item_state.submitting_input) {
input.class_name = item_state.correct_input ? 'correct' : 'incorrect';
}
},
data: null
};
return {
init: _fn.init,
}
return {
value: item.id,
drag_disabled: Boolean(item_state || state.state.finished),
width: item.size.width,
height: item.size.height,
top: position.top,
left: position.left,
position: position.absolute ? 'absolute' : 'relative',
class_name: item_state && ('input' in item_state || item_state.correct_input) ? 'fade': undefined,
input: input,
content_html: item.backgroundImage ? '<img src="' + item.backgroundImage + '"/>' : item.displayName
};
});
var context = {
header_html: state.title,
question_html: state.question_text,
popup_html: state.state.feedback || '',
feedback_html: $.trim(state.state.finished ? state.feedback.finish : state.feedback.start),
target_img_src: state.targetImg,
display_zone_labels: state.displayLabels,
display_reset_button: state.state.finished,
zones: state.zones,
items: items
};
})(jQuery);
$.ajax(runtime.handlerUrl(element, 'get_data'), {
dataType: 'json'
}).done(function(data){
dragAndDrop.init(data);
});
return DragAndDropBlock.renderView(context);
};
publish_event({event_type:"xblock.drag-and-drop-v2.loaded"});
init();
}
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define("virtual-dom-1.3.0",[],e);else{var n;"undefined"!=typeof window?n=window:"undefined"!=typeof global?n=global:"undefined"!=typeof self&&(n=self),n.virtualDom=e()}}(function(){return function e(n,t,r){function o(s,u){if(!t[s]){if(!n[s]){var a="function"==typeof require&&require;if(!u&&a)return a(s,!0);if(i)return i(s,!0);var f=new Error("Cannot find module '"+s+"'");throw f.code="MODULE_NOT_FOUND",f}var v=t[s]={exports:{}};n[s][0].call(v.exports,function(e){var t=n[s][1][e];return o(t?t:e)},v,v.exports,e,n,t,r)}return t[s].exports}for(var i="function"==typeof require&&require,s=0;s<r.length;s++)o(r[s]);return o}({1:[function(e,n){var t=e("./vdom/create-element.js");n.exports=t},{"./vdom/create-element.js":15}],2:[function(e,n){var t=e("./vtree/diff.js");n.exports=t},{"./vtree/diff.js":35}],3:[function(e,n){var t=e("./virtual-hyperscript/index.js");n.exports=t},{"./virtual-hyperscript/index.js":22}],4:[function(e,n){var t=e("./diff.js"),r=e("./patch.js"),o=e("./h.js"),i=e("./create-element.js");n.exports={diff:t,patch:r,h:o,create:i}},{"./create-element.js":1,"./diff.js":2,"./h.js":3,"./patch.js":13}],5:[function(e,n){n.exports=function(e){var n,t=String.prototype.split,r=/()??/.exec("")[1]===e;return n=function(n,o,i){if("[object RegExp]"!==Object.prototype.toString.call(o))return t.call(n,o,i);var s,u,a,f,v=[],d=(o.ignoreCase?"i":"")+(o.multiline?"m":"")+(o.extended?"x":"")+(o.sticky?"y":""),c=0,o=new RegExp(o.source,d+"g");for(n+="",r||(s=new RegExp("^"+o.source+"$(?!\\s)",d)),i=i===e?-1>>>0:i>>>0;(u=o.exec(n))&&(a=u.index+u[0].length,!(a>c&&(v.push(n.slice(c,u.index)),!r&&u.length>1&&u[0].replace(s,function(){for(var n=1;n<arguments.length-2;n++)arguments[n]===e&&(u[n]=e)}),u.length>1&&u.index<n.length&&Array.prototype.push.apply(v,u.slice(1)),f=u[0].length,c=a,v.length>=i)));)o.lastIndex===u.index&&o.lastIndex++;return c===n.length?(f||!o.test(""))&&v.push(""):v.push(n.slice(c)),v.length>i?v.slice(0,i):v}}()},{}],6:[function(){},{}],7:[function(e,n){"use strict";function t(e){var n=e[i];return n||(n=e[i]={}),n}var r=e("individual/one-version"),o="7";r("ev-store",o);var i="__EV_STORE_KEY@"+o;n.exports=t},{"individual/one-version":9}],8:[function(e,n){(function(e){"use strict";function t(e,n){return e in r?r[e]:(r[e]=n,n)}var r="undefined"!=typeof window?window:"undefined"!=typeof e?e:{};n.exports=t}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],9:[function(e,n){"use strict";function t(e,n,t){var o="__INDIVIDUAL_ONE_VERSION_"+e,i=o+"_ENFORCE_SINGLETON",s=r(i,n);if(s!==n)throw new Error("Can only have one copy of "+e+".\nYou already have version "+s+" installed.\nThis means you cannot install version "+n);return r(o,t)}var r=e("./index.js");n.exports=t},{"./index.js":8}],10:[function(e,n){(function(t){var r="undefined"!=typeof t?t:"undefined"!=typeof window?window:{},o=e("min-document");if("undefined"!=typeof document)n.exports=document;else{var i=r["__GLOBAL_DOCUMENT_CACHE@4"];i||(i=r["__GLOBAL_DOCUMENT_CACHE@4"]=o),n.exports=i}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"min-document":6}],11:[function(e,n){"use strict";n.exports=function(e){return"object"==typeof e&&null!==e}},{}],12:[function(e,n){function t(e){return"[object Array]"===o.call(e)}var r=Array.isArray,o=Object.prototype.toString;n.exports=r||t},{}],13:[function(e,n){var t=e("./vdom/patch.js");n.exports=t},{"./vdom/patch.js":18}],14:[function(e,n){function t(e,n,t){for(var i in n){var a=n[i];void 0===a?r(e,i,a,t):u(a)?(r(e,i,a,t),a.hook&&a.hook(e,i,t?t[i]:void 0)):s(a)?o(e,n,t,i,a):e[i]=a}}function r(e,n,t,r){if(r){var o=r[n];if(u(o))o.unhook&&o.unhook(e,n,t);else if("attributes"===n)for(var i in o)e.removeAttribute(i);else if("style"===n)for(var s in o)e.style[s]="";else e[n]="string"==typeof o?"":null}}function o(e,n,t,r,o){var u=t?t[r]:void 0;if("attributes"!==r){if(u&&s(u)&&i(u)!==i(o))return void(e[r]=o);s(e[r])||(e[r]={});var a="style"===r?"":void 0;for(var f in o){var v=o[f];e[r][f]=void 0===v?a:v}}else for(var d in o){var c=o[d];void 0===c?e.removeAttribute(d):e.setAttribute(d,c)}}function i(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var s=e("is-object"),u=e("../vnode/is-vhook.js");n.exports=t},{"../vnode/is-vhook.js":26,"is-object":11}],15:[function(e,n){function t(e,n){var f=n?n.document||r:r,v=n?n.warn:null;if(e=a(e).a,u(e))return e.init();if(s(e))return f.createTextNode(e.text);if(!i(e))return v&&v("Item is not a valid virtual dom node",e),null;var d=null===e.namespace?f.createElement(e.tagName):f.createElementNS(e.namespace,e.tagName),c=e.properties;o(d,c);for(var p=e.children,l=0;l<p.length;l++){var h=t(p[l],n);h&&d.appendChild(h)}return d}var r=e("global/document"),o=e("./apply-properties"),i=e("../vnode/is-vnode.js"),s=e("../vnode/is-vtext.js"),u=e("../vnode/is-widget.js"),a=e("../vnode/handle-thunk.js");n.exports=t},{"../vnode/handle-thunk.js":24,"../vnode/is-vnode.js":27,"../vnode/is-vtext.js":28,"../vnode/is-widget.js":29,"./apply-properties":14,"global/document":10}],16:[function(e,n){function t(e,n,t,o){return t&&0!==t.length?(t.sort(i),r(e,n,t,o,0)):{}}function r(e,n,t,i,u){if(i=i||{},e){o(t,u,u)&&(i[u]=e);var a=n.children;if(a)for(var f=e.childNodes,v=0;v<n.children.length;v++){u+=1;var d=a[v]||s,c=u+(d.count||0);o(t,u,c)&&r(f[v],d,t,i,u),u=c}}return i}function o(e,n,t){if(0===e.length)return!1;for(var r,o,i=0,s=e.length-1;s>=i;){if(r=(s+i)/2>>0,o=e[r],i===s)return o>=n&&t>=o;if(n>o)i=r+1;else{if(!(o>t))return!0;s=r-1}}return!1}function i(e,n){return e>n?1:-1}var s={};n.exports=t},{}],17:[function(e,n){function t(e,n,t){var a=e.type,c=e.vNode,l=e.patch;switch(a){case p.REMOVE:return r(n,c);case p.INSERT:return o(n,l,t);case p.VTEXT:return i(n,c,l,t);case p.WIDGET:return s(n,c,l,t);case p.VNODE:return u(n,c,l,t);case p.ORDER:return f(n,l),n;case p.PROPS:return d(n,l,c.properties),n;case p.THUNK:return v(n,t.patch(n,l,t));default:return n}}function r(e,n){var t=e.parentNode;return t&&t.removeChild(e),a(e,n),null}function o(e,n,t){var r=l(n,t);return e&&e.appendChild(r),e}function i(e,n,t,r){var o;if(3===e.nodeType)e.replaceData(0,e.length,t.text),o=e;else{var i=e.parentNode;o=l(t,r),i&&i.replaceChild(o,e)}return o}function s(e,n,t,r){var o,i=h(n,t);o=i?t.update(n,e)||e:l(t,r);var s=e.parentNode;return s&&o!==e&&s.replaceChild(o,e),i||a(e,n),o}function u(e,n,t,r){var o=e.parentNode,i=l(t,r);return o&&o.replaceChild(i,e),i}function a(e,n){"function"==typeof n.destroy&&c(n)&&n.destroy(e)}function f(e,n){var t,r=[],o=e.childNodes,i=o.length,s=n.reverse;for(t=0;i>t;t++)r.push(e.childNodes[t]);var u,a,f,v,d,c=0;for(t=0;i>t;){if(u=n[t],v=1,void 0!==u&&u!==t){for(;n[t+v]===u+v;)v++;for(s[t]>t+v&&c++,a=r[u],f=o[t+c]||null,d=0;a!==f&&d++<v;)e.insertBefore(a,f),a=r[u+d];t>u+v&&c--}t in n.removes&&c++,t+=v}}function v(e,n){return e&&n&&e!==n&&e.parentNode&&(console.log(e),e.parentNode.replaceChild(n,e)),n}var d=e("./apply-properties"),c=e("../vnode/is-widget.js"),p=e("../vnode/vpatch.js"),l=e("./create-element"),h=e("./update-widget");n.exports=t},{"../vnode/is-widget.js":29,"../vnode/vpatch.js":32,"./apply-properties":14,"./create-element":15,"./update-widget":19}],18:[function(e,n){function t(e,n){return r(e,n)}function r(e,n,t){var u=i(n);if(0===u.length)return e;var f=a(e,n.a,u),v=e.ownerDocument;t||(t={patch:r},v!==s&&(t.document=v));for(var d=0;d<u.length;d++){var c=u[d];e=o(e,f[c],n[c],t)}return e}function o(e,n,t,r){if(!n)return e;var o;if(u(t))for(var i=0;i<t.length;i++)o=f(t[i],n,r),n===e&&(e=o);else o=f(t,n,r),n===e&&(e=o);return e}function i(e){var n=[];for(var t in e)"a"!==t&&n.push(Number(t));return n}var s=e("global/document"),u=e("x-is-array"),a=e("./dom-index"),f=e("./patch-op");n.exports=t},{"./dom-index":16,"./patch-op":17,"global/document":10,"x-is-array":12}],19:[function(e,n){function t(e,n){return r(e)&&r(n)?"name"in e&&"name"in n?e.id===n.id:e.init===n.init:!1}var r=e("../vnode/is-widget.js");n.exports=t},{"../vnode/is-widget.js":29}],20:[function(e,n){"use strict";function t(e){return this instanceof t?void(this.value=e):new t(e)}var r=e("ev-store");n.exports=t,t.prototype.hook=function(e,n){var t=r(e),o=n.substr(3);t[o]=this.value},t.prototype.unhook=function(e,n){var t=r(e),o=n.substr(3);t[o]=void 0}},{"ev-store":7}],21:[function(e,n){"use strict";function t(e){return this instanceof t?void(this.value=e):new t(e)}n.exports=t,t.prototype.hook=function(e,n){e[n]!==this.value&&(e[n]=this.value)}},{}],22:[function(e,n){"use strict";function t(e,n,t){var i,u,a,f,d=[];return!t&&s(n)&&(t=n,u={}),u=u||n||{},i=g(e,u),u.hasOwnProperty("key")&&(a=u.key,u.key=void 0),u.hasOwnProperty("namespace")&&(f=u.namespace,u.namespace=void 0),"INPUT"!==i||f||!u.hasOwnProperty("value")||void 0===u.value||h(u.value)||(u.value=x(u.value)),o(u),void 0!==t&&null!==t&&r(t,d,i,u),new v(i,u,d,a,f)}function r(e,n,t,o){if("string"==typeof e)n.push(new d(e));else if(i(e))n.push(e);else{if(!f(e)){if(null===e||void 0===e)return;throw u({foreignObject:e,parentVnode:{tagName:t,properties:o}})}for(var s=0;s<e.length;s++)r(e[s],n,t,o)}}function o(e){for(var n in e)if(e.hasOwnProperty(n)){var t=e[n];if(h(t))continue;"ev-"===n.substr(0,3)&&(e[n]=w(t))}}function i(e){return c(e)||p(e)||l(e)||y(e)}function s(e){return"string"==typeof e||f(e)||i(e)}function u(e){var n=new Error;return n.type="virtual-hyperscript.unexpected.virtual-element",n.message="Unexpected virtual child passed to h().\nExpected a VNode / Vthunk / VWidget / string but:\ngot:\n"+a(e.foreignObject)+".\nThe parent vnode is:\n"+a(e.parentVnode),n.foreignObject=e.foreignObject,n.parentVnode=e.parentVnode,n}function a(e){try{return JSON.stringify(e,null," ")}catch(n){return String(e)}}var f=e("x-is-array"),v=e("../vnode/vnode.js"),d=e("../vnode/vtext.js"),c=e("../vnode/is-vnode"),p=e("../vnode/is-vtext"),l=e("../vnode/is-widget"),h=e("../vnode/is-vhook"),y=e("../vnode/is-thunk"),g=e("./parse-tag.js"),x=e("./hooks/soft-set-hook.js"),w=e("./hooks/ev-hook.js");n.exports=t},{"../vnode/is-thunk":25,"../vnode/is-vhook":26,"../vnode/is-vnode":27,"../vnode/is-vtext":28,"../vnode/is-widget":29,"../vnode/vnode.js":31,"../vnode/vtext.js":33,"./hooks/ev-hook.js":20,"./hooks/soft-set-hook.js":21,"./parse-tag.js":23,"x-is-array":12}],23:[function(e,n){"use strict";function t(e,n){if(!e)return"DIV";var t=!n.hasOwnProperty("id"),s=r(e,o),u=null;i.test(s[1])&&(u="DIV");var a,f,v,d;for(d=0;d<s.length;d++)f=s[d],f&&(v=f.charAt(0),u?"."===v?(a=a||[],a.push(f.substring(1,f.length))):"#"===v&&t&&(n.id=f.substring(1,f.length)):u=f);return a&&(n.className&&a.push(n.className),n.className=a.join(" ")),n.namespace?u:u.toUpperCase()}var r=e("browser-split"),o=/([\.#]?[a-zA-Z0-9_:-]+)/,i=/^\.|#/;n.exports=t},{"browser-split":5}],24:[function(e,n){function t(e,n){var t=e,o=n;return u(n)&&(o=r(n,e)),u(e)&&(t=r(e,null)),{a:t,b:o}}function r(e,n){var t=e.vnode;if(t||(t=e.vnode=e.render(n)),!(o(t)||i(t)||s(t)))throw new Error("thunk did not return a valid node");return t}var o=e("./is-vnode"),i=e("./is-vtext"),s=e("./is-widget"),u=e("./is-thunk");n.exports=t},{"./is-thunk":25,"./is-vnode":27,"./is-vtext":28,"./is-widget":29}],25:[function(e,n){function t(e){return e&&"Thunk"===e.type}n.exports=t},{}],26:[function(e,n){function t(e){return e&&("function"==typeof e.hook&&!e.hasOwnProperty("hook")||"function"==typeof e.unhook&&!e.hasOwnProperty("unhook"))}n.exports=t},{}],27:[function(e,n){function t(e){return e&&"VirtualNode"===e.type&&e.version===r}var r=e("./version");n.exports=t},{"./version":30}],28:[function(e,n){function t(e){return e&&"VirtualText"===e.type&&e.version===r}var r=e("./version");n.exports=t},{"./version":30}],29:[function(e,n){function t(e){return e&&"Widget"===e.type}n.exports=t},{}],30:[function(e,n){n.exports="1"},{}],31:[function(e,n){function t(e,n,t,r,v){this.tagName=e,this.properties=n||a,this.children=t||f,this.key=null!=r?String(r):void 0,this.namespace="string"==typeof v?v:null;var d,c=t&&t.length||0,p=0,l=!1,h=!1,y=!1;for(var g in n)if(n.hasOwnProperty(g)){var x=n[g];u(x)&&x.unhook&&(d||(d={}),d[g]=x)}for(var w=0;c>w;w++){var m=t[w];o(m)?(p+=m.count||0,!l&&m.hasWidgets&&(l=!0),!h&&m.hasThunks&&(h=!0),y||!m.hooks&&!m.descendantHooks||(y=!0)):!l&&i(m)?"function"==typeof m.destroy&&(l=!0):!h&&s(m)&&(h=!0)}this.count=c+p,this.hasWidgets=l,this.hasThunks=h,this.hooks=d,this.descendantHooks=y}var r=e("./version"),o=e("./is-vnode"),i=e("./is-widget"),s=e("./is-thunk"),u=e("./is-vhook");n.exports=t;var a={},f=[];t.prototype.version=r,t.prototype.type="VirtualNode"},{"./is-thunk":25,"./is-vhook":26,"./is-vnode":27,"./is-widget":29,"./version":30}],32:[function(e,n){function t(e,n,t){this.type=Number(e),this.vNode=n,this.patch=t}var r=e("./version");t.NONE=0,t.VTEXT=1,t.VNODE=2,t.WIDGET=3,t.PROPS=4,t.ORDER=5,t.INSERT=6,t.REMOVE=7,t.THUNK=8,n.exports=t,t.prototype.version=r,t.prototype.type="VirtualPatch"},{"./version":30}],33:[function(e,n){function t(e){this.text=String(e)}var r=e("./version");n.exports=t,t.prototype.version=r,t.prototype.type="VirtualText"},{"./version":30}],34:[function(e,n){function t(e,n){var s;for(var u in e){u in n||(s=s||{},s[u]=void 0);var a=e[u],f=n[u];if(a!==f)if(o(a)&&o(f))if(r(f)!==r(a))s=s||{},s[u]=f;else if(i(f))s=s||{},s[u]=f;else{var v=t(a,f);v&&(s=s||{},s[u]=v)}else s=s||{},s[u]=f}for(var d in n)d in e||(s=s||{},s[d]=n[d]);return s}function r(e){return Object.getPrototypeOf?Object.getPrototypeOf(e):e.__proto__?e.__proto__:e.constructor?e.constructor.prototype:void 0}var o=e("is-object"),i=e("../vnode/is-vhook");n.exports=t},{"../vnode/is-vhook":26,"is-object":11}],35:[function(e,n){function t(e,n){var t={a:e};return r(e,n,t,0),t}function r(e,n,t,r){if(e!==n){var s=t[r],a=!1;if(w(e)||w(n))u(e,n,t,r);else if(null==n)x(e)||(i(e,t,r),s=t[r]),s=p(s,new h(h.REMOVE,e,n));else if(y(n))if(y(e))if(e.tagName===n.tagName&&e.namespace===n.namespace&&e.key===n.key){var f=j(e.properties,n.properties);f&&(s=p(s,new h(h.PROPS,e,f))),s=o(e,n,t,s,r)}else s=p(s,new h(h.VNODE,e,n)),a=!0;else s=p(s,new h(h.VNODE,e,n)),a=!0;else g(n)?g(e)?e.text!==n.text&&(s=p(s,new h(h.VTEXT,e,n))):(s=p(s,new h(h.VTEXT,e,n)),a=!0):x(n)&&(x(e)||(a=!0),s=p(s,new h(h.WIDGET,e,n)));s&&(t[r]=s),a&&i(e,t,r)}}function o(e,n,t,o,i){for(var s=e.children,u=d(s,n.children),a=s.length,f=u.length,v=a>f?a:f,c=0;v>c;c++){var l=s[c],g=u[c];i+=1,l?r(l,g,t,i):g&&(o=p(o,new h(h.INSERT,null,g))),y(l)&&l.count&&(i+=l.count)}return u.moves&&(o=p(o,new h(h.ORDER,e,u.moves))),o}function i(e,n,t){f(e,n,t),s(e,n,t)}function s(e,n,t){if(x(e))"function"==typeof e.destroy&&(n[t]=p(n[t],new h(h.REMOVE,e,null)));else if(y(e)&&(e.hasWidgets||e.hasThunks))for(var r=e.children,o=r.length,i=0;o>i;i++){var a=r[i];t+=1,s(a,n,t),y(a)&&a.count&&(t+=a.count)}else w(e)&&u(e,null,n,t)}function u(e,n,r,o){var i=m(e,n),s=t(i.a,i.b);a(s)&&(r[o]=new h(h.THUNK,null,s))}function a(e){for(var n in e)if("a"!==n)return!0;return!1}function f(e,n,t){if(y(e)){if(e.hooks&&(n[t]=p(n[t],new h(h.PROPS,e,v(e.hooks)))),e.descendantHooks||e.hasThunks)for(var r=e.children,o=r.length,i=0;o>i;i++){var s=r[i];t+=1,f(s,n,t),y(s)&&s.count&&(t+=s.count)}}else w(e)&&u(e,null,n,t)}function v(e){var n={};for(var t in e)n[t]=void 0;return n}function d(e,n){var t=c(n);if(!t)return n;var r=c(e);if(!r)return n;var o={},i={};for(var s in t)o[t[s]]=r[s];for(var u in r)i[r[u]]=t[u];for(var a=e.length,f=n.length,v=a>f?a:f,d=[],p=0,l=0,h=0,y={},g=y.removes={},x=y.reverse={},w=!1;v>p;){var m=i[l];if(void 0!==m)d[l]=n[m],m!==h&&(y[m]=h,x[h]=m,w=!0),h++;else if(l in i)d[l]=void 0,g[l]=h++,w=!0;else{for(;void 0!==o[p];)p++;if(v>p){var j=n[p];j&&(d[l]=j,p!==h&&(w=!0,y[p]=h,x[h]=p),h++),p++}}l++}return w&&(d.moves=y),d}function c(e){var n,t;for(n=0;n<e.length;n++){var r=e[n];void 0!==r.key&&(t=t||{},t[r.key]=n)}return t}function p(e,n){return e?(l(e)?e.push(n):e=[e,n],e):n}var l=e("x-is-array"),h=e("../vnode/vpatch"),y=e("../vnode/is-vnode"),g=e("../vnode/is-vtext"),x=e("../vnode/is-widget"),w=e("../vnode/is-thunk"),m=e("../vnode/handle-thunk"),j=e("./diff-props");n.exports=t},{"../vnode/handle-thunk":24,"../vnode/is-thunk":25,"../vnode/is-vnode":27,"../vnode/is-vtext":28,"../vnode/is-widget":29,"../vnode/vpatch":32,"./diff-props":34,"x-is-array":12}]},{},[4])(4)});
if (window.require && !window.virtualDom) {
require(['virtual-dom-1.3.0'], function(virtualDom) { window.virtualDom = virtualDom; });
}
(function(h) {
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 px = function(n) {
return n + 'px';
};
var renderCollection = function(template, collection, ctx) {
return collection.map(function(item) {
return template(item, ctx);
});
};
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}),
h('button.submit-input', {disabled: input.has_value}, 'ok')
])
);
};
var itemTemplate = function(item) {
return (
h('div.option', {className: item.class_name,
attributes: {'data-value': item.value, 'data-drag-disabled': item.drag_disabled},
style: {width: item.width, height: item.height,
top: item.top, left: item.left, position: item.position}}, [
h('div', {innerHTML: item.content_html}),
itemInputTemplate(item.input)
])
);
};
var zoneTemplate = function(zone, ctx) {
return (
h('div.zone', {id: zone.id, attributes: {'data-zone': zone.title},
style: {top: px(zone.y), left: px(zone.x),
width: px(zone.width), height: px(zone.height)}},
h('p', {style: {visibility: ctx.display_zone_labels ? 'visible': 'hidden'}}, zone.title))
);
};
var feedbackTemplate = function(ctx) {
var feedback_display = ctx.feedback_html ? 'block' : 'none';
var reset_button_display = ctx.display_reset_button ? 'block' : 'none';
return (
h('section.feedback', [
h('div.reset-button', {style: {display: reset_button_display}}, 'Reset exercise'),
h('div.title1', {style: {display: feedback_display}}, 'Feedback'),
h('p.message', {style: {display: feedback_display},
innerHTML: ctx.feedback_html})
])
);
};
var mainTemplate = function(ctx) {
return (
h('section.xblock--drag-and-drop', [
h('h2.problem-header', {innerHTML: ctx.header_html}),
h('section.problem', {role: 'application'}, [
h('div.title1', 'Question'),
h('p', {innerHTML: ctx.question_html})
]),
h('section.drag-container', [
h('div.items', renderCollection(itemTemplate, ctx.items, ctx)),
h('div.target', [
h('div.popup', {style: {display: ctx.popup_html ? 'block' : 'none'}}, [
h('div.close.icon-remove-sign.fa-times-circle'),
h('p.popup-content', {innerHTML: ctx.popup_html})
]),
h('div.target-img', {style: {backgroundImage: ctx.target_img_src}},
renderCollection(zoneTemplate, ctx.zones, ctx))
]),
h('div.clear')
]),
feedbackTemplate(ctx)
])
);
};
DragAndDropBlock.renderView = mainTemplate;
})(virtualDom.h);
<section class="xblock--drag-and-drop">
{{ js_templates|safe }}
<h2 class="problem-header">
{{ title|safe }}
</h2>
<section class="problem" role="application">
<div class="title1">Question</div>
<p>{{ question_text|safe }}</p>
</section>
<section class="drag-container">
<ul class="items"></ul>
<div class="target">
<div class="popup">
<div class="close icon-remove-sign fa-times-circle"></div>
<p class="popup-content"></p>
</div>
<div class="target-img"></div>
</div>
<div class="clear"></div>
</section>
<section class="feedback">
<div class="reset-button">Reset exercise</div>
<div class="title1">Feedback</div>
<p class="message"></p>
</section>
</section>
<section class="xblock--drag-and-drop"></section>
<script id="item-tpl" type="text/html">
<li class="option" data-value="{{ id }}"
style="width: {{ size.width }}; height: {{ size.height }}">
{{{ displayName }}}
{{#if inputOptions}}
<div class="numerical-input">
<input class="input" type="text" />
<button class="submit-input">ok</button>
</div>
{{/if}}
</li>
</script>
<script id="image-item-tpl" type="text/html">
<li class="option" data-value="{{ id }}"
style="width: {{ size.width }}; height: {{ size.height }}">
<img src="{{ backgroundImage }}" />
{{#if inputOptions}}
<input class="input" type="text" />
<button class="submit-input">ok</button>
{{/if}}
</li>
</script>
<script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y }}px;
......
......@@ -3,17 +3,11 @@
# Imports ###########################################################
import logging
import pkg_resources
from django.template import Context, Template
# Globals ###########################################################
log = logging.getLogger(__name__)
# Functions #########################################################
def load_resource(resource_path):
......@@ -23,11 +17,13 @@ def load_resource(resource_path):
resource_content = pkg_resources.resource_string(__name__, resource_path)
return resource_content
def render_template(template_path, context={}):
def render_template(template_path, context=None):
"""
Evaluate a template by resource path, applying the provided context
"""
if context is None:
context = {}
template_str = load_resource(template_path)
template = Template(template_str)
return template.render(Context(context))
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git#egg=xblock-sdk
pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt
pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt
python setup.py develop
[REPORTS]
reports=no
[FORMAT]
max-line-length=120
[MESSAGES CONTROL]
disable=
locally-disabled,
missing-docstring,
too-many-ancestors,
too-many-public-methods,
unused-argument
[SIMILARITIES]
min-similarity-lines=8
#!/usr/bin/env python
"""
Run tests for the Drag and Drop V2 XBlock.
This script is required to run our selenium tests inside the xblock-sdk workbench
because the workbench SDK's settings file is not inside any python module.
"""
import os
import sys
import workbench
if __name__ == "__main__":
# Find the location of the XBlock SDK. Note: it must be installed in development mode.
# ('python setup.py develop' or 'pip install -e')
xblock_sdk_dir = os.path.dirname(os.path.dirname(workbench.__file__))
sys.path.append(xblock_sdk_dir)
# Use the workbench settings file:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault("DJANGO_LIVE_TEST_SERVER_ADDRESS", "localhost:8081-8099")
from django.core.management import execute_from_command_line
args = sys.argv[1:]
paths = [arg for arg in args if arg[0] != '-']
if not paths:
paths = ["tests/"]
options = [arg for arg in args if arg not in paths]
execute_from_command_line([sys.argv[0], "test"] + paths + options)
......@@ -28,7 +28,10 @@ setup(
packages=['drag_and_drop_v2'],
install_requires=[
'XBlock',
'xblock-utils',
'ddt'
],
dependency_links = ['http://github.com/edx/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock',
},
......
......@@ -89,5 +89,7 @@
"start": "Intro Feed",
"finish": "Final Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"question_text": ""
}
......@@ -68,5 +68,7 @@
"feedback": {
"start": "Intro Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"question_text": ""
}
......@@ -68,5 +68,7 @@
"feedback": {
"start": "Intro Feed"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"question_text": ""
}
......@@ -89,5 +89,7 @@
"start": "Intro Feed",
"finish": "Final <b>Feed</b>"
},
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png"
"targetImg": "http://i0.kym-cdn.com/photos/images/newsfeed/000/030/404/1260585284155.png",
"title": "Drag and Drop",
"question_text": ""
}
......@@ -4,32 +4,21 @@ from selenium.webdriver.support.ui import WebDriverWait
from tests.utils import load_resource
from workbench import scenarios
from workbench.test.selenium_test import SeleniumTest
from xblockutils.base_test import SeleniumBaseTest
# Classes ###########################################################
class BaseIntegrationTest(SeleniumTest):
class BaseIntegrationTest(SeleniumBaseTest):
default_css_selector = 'section.xblock--drag-and-drop'
module_name = __name__
_additional_escapes = {
'"': "&quot;",
"'": "&apos;"
}
def setUp(self):
super(BaseIntegrationTest, self).setUp()
# Use test scenarios
self.browser.get(self.live_server_url) # Needed to load tests once
scenarios.SCENARIOS.clear()
# Suzy opens the browser to visit the workbench
self.browser.get(self.live_server_url)
# She knows it's the site by the header
header1 = self.browser.find_element_by_css_selector('h1')
self.assertEqual(header1.text, 'XBlock scenarios')
def _make_scenario_xml(self, display_name, question_text, completed):
return """
<vertical_demo>
......@@ -48,8 +37,8 @@ class BaseIntegrationTest(SeleniumTest):
self.addCleanup(scenarios.remove_scenario, identifier)
def _get_items(self):
items_container = self._page.find_element_by_css_selector('ul.items')
return items_container.find_elements_by_css_selector('li.option')
items_container = self._page.find_element_by_css_selector('.items')
return items_container.find_elements_by_css_selector('.option')
def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone")
......@@ -57,25 +46,13 @@ class BaseIntegrationTest(SeleniumTest):
def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message")
def go_to_page(self, page_name, css_selector='section.xblock--drag-and-drop'):
"""
Navigate to the page `page_name`, as listed on the workbench home
Returns the DOM element on the visited page located by the `css_selector`
"""
self.browser.get(self.live_server_url)
self.browser.find_element_by_link_text(page_name).click()
return self.browser.find_element_by_css_selector(css_selector)
def get_element_html(self, element):
return element.get_attribute('innerHTML').strip()
def get_element_classes(self, element):
return element.get_attribute('class').split()
def scroll_to(self, y):
self.browser.execute_script('window.scrollTo(0, {0})'.format(y))
def wait_until_contains_html(self, html, elem):
def wait_until_html_in(self, html, elem):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: html in e.get_attribute('innerHTML'),
u"{} should be in {}".format(html, elem.get_attribute('innerHTML')))
......@@ -84,4 +61,3 @@ class BaseIntegrationTest(SeleniumTest):
wait = WebDriverWait(elem, 2)
wait.until(lambda e: class_name in e.get_attribute('class').split(),
u"Class name {} not in {}".format(class_name, elem.get_attribute('class')))
......@@ -48,15 +48,17 @@ class InteractionTestFixture(BaseIntegrationTest):
scenario_xml = self._get_scenario_xml()
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, scenario_xml)
self.browser.get(self.live_server_url)
self._page = self.go_to_page(self.PAGE_TITLE)
# Resize window so that the entire drag container is visible.
# Selenium has issues when dragging to an area that is off screen.
self.browser.set_window_size(1024, 800)
def _get_item_by_value(self, item_value):
items_container = self._page.find_element_by_css_selector('ul.items')
return items_container.find_elements_by_xpath("//li[@data-value='{item_id}']".format(item_id=item_value))[0]
items_container = self._page.find_element_by_css_selector('.items')
return items_container.find_elements_by_xpath("//div[@data-value='{item_id}']".format(item_id=item_value))[0]
def _get_zone_by_id(self, zone_id):
zones_container = self._page.find_element_by_css_selector('div.target')
zones_container = self._page.find_element_by_css_selector('.target')
return zones_container.find_elements_by_xpath("//div[@data-zone='{zone_id}']".format(zone_id=zone_id))[0]
def _get_input_div_by_value(self, item_value):
......@@ -68,7 +70,6 @@ class InteractionTestFixture(BaseIntegrationTest):
element.find_element_by_class_name('input').send_keys(value)
element.find_element_by_class_name('submit-input').click()
def drag_item_to_zone(self, item_value, zone_id):
element = self._get_item_by_value(item_value)
target = self._get_zone_by_id(zone_id)
......@@ -76,11 +77,11 @@ class InteractionTestFixture(BaseIntegrationTest):
action_chains.drag_and_drop(element, target).perform()
def test_item_positive_feedback_on_good_move(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content")
for definition in self._get_correct_item_for_zone().values():
if not definition.input:
self.drag_item_to_zone(definition.item_id, definition.zone_id)
self.wait_until_contains_html(definition.feedback_positive, feedback_popup)
feedback_popup = self._page.find_element_by_css_selector(".popup-content")
self.wait_until_html_in(definition.feedback_positive, feedback_popup)
def test_item_positive_feedback_on_good_input(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content")
......@@ -90,7 +91,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self._send_input(definition.item_id, definition.input)
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('correct', input_div)
self.wait_until_contains_html(definition.feedback_positive, feedback_popup)
self.wait_until_html_in(definition.feedback_positive, feedback_popup)
def test_item_negative_feedback_on_bad_move(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content")
......@@ -100,7 +101,7 @@ class InteractionTestFixture(BaseIntegrationTest):
if zone == definition.zone_id:
continue
self.drag_item_to_zone(definition.item_id, zone)
self.wait_until_contains_html(definition.feedback_negative, feedback_popup)
self.wait_until_html_in(definition.feedback_negative, feedback_popup)
def test_item_positive_feedback_on_bad_input(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content")
......@@ -110,7 +111,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self._send_input(definition.item_id, '1999999')
input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('incorrect', input_div)
self.wait_until_contains_html(definition.feedback_negative, feedback_popup)
self.wait_until_html_in(definition.feedback_negative, feedback_popup)
def test_final_feedback_and_reset(self):
feedback_message = self._get_feedback_message()
......@@ -128,16 +129,13 @@ class InteractionTestFixture(BaseIntegrationTest):
input_div = self._get_input_div_by_value(item_key)
self.wait_until_has_class('correct', input_div)
self.wait_until_contains_html(self.feedback['final'], feedback_message)
self.wait_until_exists('.reset-button')
self.wait_until_html_in(self.feedback['final'], self._get_feedback_message())
# scrolling to have `reset` visible, otherwise it does not receive a click
# this is due to xblock workbench header that consumes top 40px - selenium scrolls so page so that target
# element is a the very top.
self.scroll_to(100)
reset = self._page.find_element_by_css_selector(".reset-button")
reset = self._page.find_element_by_css_selector('.reset-button')
reset.click()
self.wait_until_contains_html(self.feedback['intro'], feedback_message)
self.wait_until_html_in(self.feedback['intro'], self._get_feedback_message())
locations_after_reset = get_locations()
for item_key in items.keys():
......
......@@ -59,4 +59,4 @@ class TestDragAndDropRender(BaseIntegrationTest):
def test_feedback(self):
feedback_message = self._get_feedback_message()
self.assertEqual(feedback_message.text, "Intro Feed")
\ No newline at end of file
self.assertEqual(feedback_message.text, "Intro Feed")
from nose_parameterized import parameterized
from ddt import ddt, unpack, data
from tests.integration.test_base import BaseIntegrationTest
from workbench import scenarios
@ddt
class TestDragAndDropTitleAndQuestion(BaseIntegrationTest):
@parameterized.expand([
@unpack
@data(
('plain1', 'title1', 'question1'),
('plain2', 'title2', 'question2'),
('html1', 'title with <i>HTML</i>', 'Question with <i>HTML</i>'),
('html2', '<span style="color:red">Title: HTML?</span>', '<span style="color:red">Span question</span>'),
])
)
def test_title_and_question_parameters(self, _, display_name, question_text):
const_page_name = 'Test block parameters'
const_page_id = 'test_block_title'
......
#!/usr/bin/env python
"""Manage.py file for xblock-drag-and-drop-v2"""
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
bok_choy==0.3.2
cookiecutter==0.7.1
coverage==3.7.1
diff-cover==0.7.2
Django==1.4.16
django_nose==1.2
fs==0.5.0
lazy==1.2
lxml==3.4.1
mock==1.0.1
nose==1.3.4
nose-parameterized==0.3.5
pep8==1.5.7
pylint==0.28
pypng==0.0.17
rednose==0.4.1
requests==2.4.3
selenium==2.44.0
simplejson==3.6.5
webob==1.4
-e git+https://github.com/open-craft/XBlock.git@3ece535ee8e095f21de2f8b28cc720145749f0d6#egg=XBlock
-e git+https://github.com/edx/acid-block.git@459aff7b63db8f2c5decd1755706c1a64fb4ebb1#egg=acid-xblock
-e git+https://github.com/pmitros/django-pyfs.git@514607d78535fd80bfd23184cd292ee5799b500d#egg=djpyfs
-e git+https://github.com/open-craft/xblock-sdk.git@39b00c58931664ce29bb4b38df827fe237a69613#egg=xblock-sdk
-e .
DEBUG = True
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'workbench',
'sample_xblocks.basic',
'django_nose',
)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'drag_and_drop_v2.db'
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.eggs.Loader',
)
ROOT_URLCONF = 'urls'
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
STATIC_ROOT = ''
STATIC_URL = '/static/'
WORKBENCH = {'reset_state_on_restart': False}
......@@ -7,6 +7,7 @@ from webob import Request
from mock import Mock
from workbench.runtime import WorkbenchRuntime
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData, DictKeyValueStore
from nose.tools import (
......@@ -33,10 +34,14 @@ def make_request(body, method='POST'):
def make_block():
runtime = WorkbenchRuntime()
block_type = 'drag_and_drop_v2'
key_store = DictKeyValueStore()
db_model = KvsFieldData(key_store)
return drag_and_drop_v2.DragAndDropBlock(runtime, db_model, Mock())
field_data = KvsFieldData(key_store)
runtime = WorkbenchRuntime()
def_id = runtime.id_generator.create_definition(block_type)
usage_id = runtime.id_generator.create_usage(def_id)
scope_ids = ScopeIds('user', block_type, def_id, usage_id)
return drag_and_drop_v2.DragAndDropBlock(runtime, field_data, scope_ids=scope_ids)
def test_templates_contents():
......@@ -46,17 +51,9 @@ def test_templates_contents():
block.question_text = "Question Drag & Drop"
block.weight = 5
student_fragment = block.render('student_view', Mock())
student_fragment = block.runtime.render(block, 'student_view', ['ingore'])# block.render('student_view', Mock())
assert_in('<section class="xblock--drag-and-drop">',
student_fragment.content)
assert_in('{{ value }}', student_fragment.content)
assert_in("Test Drag & Drop", student_fragment.content)
assert_in("Question Drag & Drop", student_fragment.content)
studio_fragment = block.render('studio_view', Mock())
assert_in('<div class="xblock--drag-and-drop editor-with-buttons">',
studio_fragment.content)
assert_in('{{ value }}', studio_fragment.content)
def test_studio_submit():
block = make_block()
......@@ -176,7 +173,7 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response()
expected["state"] = {
"items": {
"1": {"top": "22px", "left": "222px", "correct_input": False}
"1": {"top": "22px", "left": "222px", "absolute": True, "correct_input": False}
},
"finished": False
}
......@@ -196,7 +193,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response()
expected["state"] = {
"items": {
"1": {"top": "22px", "left": "222px", "input": "250", "correct_input": False}
"1": {"top": "22px", "left": "222px", "absolute": True,
"input": "250", "correct_input": False}
},
"finished": False
}
......@@ -216,7 +214,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response()
expected["state"] = {
"items": {
"1": {"top": "22px", "left": "222px", "input": "103", "correct_input": True}
"1": {"top": "22px", "left": "222px", "absolute": True,
"input": "103", "correct_input": True}
},
"finished": False
}
......@@ -255,7 +254,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response()
expected["state"] = {
"items": {
"0": {"top": "11px", "left": "111px", "correct_input": True}
"0": {"top": "11px", "left": "111px", "absolute": True,
"correct_input": True}
},
"finished": False
}
......@@ -277,8 +277,9 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response()
expected["state"] = {
"items": {
"0": {"top": "11px", "left": "111px", "correct_input": True},
"1": {"top": "22px", "left": "222px", "input": "99", "correct_input": True}
"0": {"top": "11px", "left": "111px", "absolute": True, "correct_input": True},
"1": {"top": "22px", "left": "222px", "absolute": True, "input": "99",
"correct_input": True}
},
"finished": True
}
......@@ -336,8 +337,8 @@ def test_ajax_solve_and_reset():
block.handle('do_attempt', make_request(data))
assert_true(block.completed)
assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px"},
'1': {"top": "22px", "left": "222px"}})
assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px", "absolute": True},
'1': {"top": "22px", "left": "222px", "absolute": True}})
block.handle('reset', make_request("{}"))
......
from django.conf.urls import include, url
urlpatterns = [
url(r'^', include('workbench.urls')),
]
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