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. ...@@ -93,22 +93,15 @@ You can define an arbitrary number of drag items.
Testing Testing
------- -------
In a virtualenv, run Inside a fresh virtualenv, run
```bash ```bash
$ cd .../xblock-drag-and-drop-v2/ $ 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: To run the tests, from the xblock-drag-and-drop-v2 repository root:
```bash ```bash
$ tests/manage.py test --rednose $ python run_tests.py
```
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
``` ```
default_data = { DEFAULT_DATA = {
"zones": [ "zones": [
{ {
"index": 1, "index": 1,
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
# Imports ########################################################### # Imports ###########################################################
import logging
import json import json
import webob import webob
import copy import copy
...@@ -14,12 +13,7 @@ from xblock.fields import Scope, String, Dict, Float, Boolean ...@@ -14,12 +13,7 @@ from xblock.fields import Scope, String, Dict, Float, Boolean
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .utils import render_template, load_resource from .utils import render_template, load_resource
from .default_data import default_data from .default_data import DEFAULT_DATA
# Globals ###########################################################
log = logging.getLogger(__name__)
# Classes ########################################################### # Classes ###########################################################
...@@ -53,7 +47,7 @@ class DragAndDropBlock(XBlock): ...@@ -53,7 +47,7 @@ class DragAndDropBlock(XBlock):
display_name="Drag and Drop", display_name="Drag and Drop",
help="JSON spec as generated by the builder", help="JSON spec as generated by the builder",
scope=Scope.content, scope=Scope.content,
default=default_data default=DEFAULT_DATA
) )
item_state = Dict( item_state = Dict(
...@@ -75,30 +69,22 @@ class DragAndDropBlock(XBlock): ...@@ -75,30 +69,22 @@ class DragAndDropBlock(XBlock):
Player view, displayed to the student 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 = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop.html', context)) fragment.add_content(render_template('/templates/html/drag_and_drop.html'))
CSS_URLS = ( css_urls = (
'public/css/vendor/jquery-ui-1.10.4.custom.min.css', 'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop.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-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-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/virtual-dom-1.3.0.min.js',
'public/js/vendor/handlebars-v1.1.2.js',
'public/js/drag_and_drop.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)) 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.add_javascript_url(self.runtime.local_resource_url(self, js_url))
fragment.initialize_js('DragAndDropBlock') fragment.initialize_js('DragAndDropBlock')
...@@ -119,18 +105,21 @@ class DragAndDropBlock(XBlock): ...@@ -119,18 +105,21 @@ class DragAndDropBlock(XBlock):
fragment = Fragment() fragment = Fragment()
fragment.add_content(render_template('/templates/html/drag_and_drop_edit.html', context)) 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')) css_urls = (
fragment.add_css_url(self.runtime.local_resource_url(self, 'public/css/vendor/jquery-ui-1.10.4.custom.min.css',
'public/css/drag_and_drop_edit.css')) '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')) js_urls = (
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vendor/jquery-ui-1.10.4.custom.min.js',
'public/js/vendor/jquery.html5-placeholder-shim.js')) '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',
'public/js/vendor/handlebars-v1.1.2.js')) 'public/js/drag_and_drop_edit.js',
fragment.add_javascript_url(self.runtime.local_resource_url(self, )
'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') fragment.initialize_js('DragAndDropEditBlock')
...@@ -149,33 +138,12 @@ class DragAndDropBlock(XBlock): ...@@ -149,33 +138,12 @@ class DragAndDropBlock(XBlock):
@XBlock.handler @XBlock.handler
def get_data(self, request, suffix=''): def get_data(self, request, suffix=''):
data = copy.deepcopy(self.data) data = self._get_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()
}
return webob.response.Response(body=json.dumps(data)) return webob.response.Response(body=json.dumps(data))
@XBlock.json_handler @XBlock.json_handler
def do_attempt(self, attempt, suffix=''): def do_attempt(self, attempt, suffix=''):
item = next(i for i in self.data['items'] if i['id'] == attempt['val']) 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 state = None
feedback = item['feedback']['incorrect'] feedback = item['feedback']['incorrect']
...@@ -195,7 +163,7 @@ class DragAndDropBlock(XBlock): ...@@ -195,7 +163,7 @@ class DragAndDropBlock(XBlock):
is_correct = False is_correct = False
elif item['zone'] == attempt['zone']: elif item['zone'] == attempt['zone']:
is_correct_location = True is_correct_location = True
if item.has_key('inputOptions'): if 'inputOptions' in item:
# Input value will have to be provided for the item. # Input value will have to be provided for the item.
# It is not (yet) correct and no feedback should be shown yet. # It is not (yet) correct and no feedback should be shown yet.
is_correct = False is_correct = False
...@@ -204,7 +172,11 @@ class DragAndDropBlock(XBlock): ...@@ -204,7 +172,11 @@ class DragAndDropBlock(XBlock):
# If this item has no input value set, we are done with it. # If this item has no input value set, we are done with it.
is_correct = True is_correct = True
feedback = item['feedback']['correct'] 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: if state:
self.item_state[str(item['id'])] = state self.item_state[str(item['id'])] = state
...@@ -247,7 +219,34 @@ class DragAndDropBlock(XBlock): ...@@ -247,7 +219,34 @@ class DragAndDropBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def reset(self, data, suffix=''): def reset(self, data, suffix=''):
self.item_state = {} 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): def _get_item_state(self):
""" """
...@@ -264,24 +263,6 @@ class DragAndDropBlock(XBlock): ...@@ -264,24 +263,6 @@ class DragAndDropBlock(XBlock):
return state 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): def _get_grade(self):
""" """
Returns the student's grade for this block. Returns the student's grade for this block.
...@@ -313,8 +294,8 @@ class DragAndDropBlock(XBlock): ...@@ -313,8 +294,8 @@ class DragAndDropBlock(XBlock):
total_count += 1 total_count += 1
item_id = str(item['id']) item_id = str(item['id'])
if item_id in item_state: if item_id in item_state:
if item.has_key('inputOptions'): if 'inputOptions' in item:
if item_state[item_id].has_key('input'): if 'input' in item_state[item_id]:
completed_count += 1 completed_count += 1
else: else:
completed_count += 1 completed_count += 1
...@@ -325,24 +306,43 @@ class DragAndDropBlock(XBlock): ...@@ -325,24 +306,43 @@ class DragAndDropBlock(XBlock):
def publish_event(self, data, suffix=''): def publish_event(self, data, suffix=''):
try: try:
event_type = data.pop('event_type') event_type = data.pop('event_type')
except KeyError as e: except KeyError:
return {'result': 'error', 'message': 'Missing event_type in JSON data'} return {'result': 'error', 'message': 'Missing event_type in JSON data'}
data['user_id'] = self.scope_ids.user_id data['user_id'] = self.scope_ids.user_id
data['component_id'] = self._get_unique_id() data['component_id'] = self._get_unique_id()
self.runtime.publish(self, event_type, data) self.runtime.publish(self, event_type, data)
return {'result':'success'} return {'result': 'success'}
def _get_unique_id(self): def _get_unique_id(self):
try: try:
unique_id = self.location.name unique_id = self.location.name # pylint: disable=no-member
except AttributeError: except AttributeError:
# workaround for xblock workbench # workaround for xblock workbench
unique_id = self.parent and self.parent.replace('.', '-') unique_id = self.parent and self.parent.replace('.', '-')
return unique_id return unique_id
@staticmethod @staticmethod
def _is_correct_input(item, val):
"""
Is submitted numerical value within the tolerated margin for this item.
"""
input_options = item.get('inputOptions')
if input_options:
try:
submitted_value = float(val)
except (ValueError, TypeError):
return False
else:
expected_value = input_options['value']
margin = input_options['margin']
return abs(submitted_value - expected_value) <= margin
else:
return True
@staticmethod
def workbench_scenarios(): def workbench_scenarios():
""" """
A canned scenario for display in the workbench. A canned scenario for display in the workbench.
......
...@@ -47,15 +47,15 @@ ...@@ -47,15 +47,15 @@
width: 210px; width: 210px;
margin: 10px; margin: 10px;
padding: 0 !important; /* LMS tries to override this */ padding: 0 !important; /* LMS tries to override this */
font-size: 14px;
position: relative; position: relative;
display: inline; display: inline;
float: left; float: left;
list-style-type: none; list-style-type: none;
} }
.xblock--drag-and-drop .drag-container .items .option { .xblock--drag-and-drop .drag-container .option {
width: 190px; width: 190px;
font-size: 14px;
background: #2e83cd; background: #2e83cd;
color: #fff; color: #fff;
position: relative; position: relative;
...@@ -68,28 +68,23 @@ ...@@ -68,28 +68,23 @@
opacity: 1; opacity: 1;
} }
.xblock--drag-and-drop .drag-container .items .option img { .xblock--drag-and-drop .drag-container .option img {
max-width: 100%; max-width: 100%;
} }
.xblock--drag-and-drop .drag-container .items .option .numerical-input { .xblock--drag-and-drop .drag-container .option .numerical-input {
display: none;
height: 32px; height: 32px;
position: absolute; position: absolute;
left: calc(100% + 5px); left: calc(100% + 5px);
top: calc(50% - 16px); top: calc(50% - 16px);
} }
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input { .xblock--drag-and-drop .drag-container .option .numerical-input .input {
display: block;
}
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input .input {
display: inline-block; display: inline-block;
width: 144px; 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; box-sizing: border-box;
position: absolute; position: absolute;
left: 150px; left: 150px;
...@@ -97,22 +92,22 @@ ...@@ -97,22 +92,22 @@
height: 24px; height: 24px;
} }
.xblock--drag-and-drop .drag-container .items .option.within-dropzone .numerical-input.correct .input-submit, .xblock--drag-and-drop .drag-container .option .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.incorrect .input-submit {
display: none; 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; background: #ceffce;
color: #0dad0d; 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; background: #ffcece;
color: #ad0d0d; color: #ad0d0d;
} }
.xblock--drag-and-drop .drag-container .items .option.fade { .xblock--drag-and-drop .drag-container .option.fade {
opacity: 0.5; opacity: 0.5;
} }
...@@ -129,7 +124,6 @@ ...@@ -129,7 +124,6 @@
} }
.xblock--drag-and-drop .target-img { .xblock--drag-and-drop .target-img {
display: none;
background: url('../img/triangle.png') no-repeat; background: url('../img/triangle.png') no-repeat;
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -229,5 +223,4 @@ ...@@ -229,5 +223,4 @@
float: right; float: right;
color: #3384CA; color: #3384CA;
margin-top: 3px; margin-top: 3px;
display: none;
} }
function DragAndDropBlock(runtime, element) { function DragAndDropBlock(runtime, element) {
function publish_event(data) { var root = $(element).find('.xblock--drag-and-drop')[0];
$.ajax({
type: "POST",
url: runtime.handlerUrl(element, 'publish_event'),
data: JSON.stringify(data)
});
}
var dragAndDrop = (function($) {
var _fn = {
pupup_ts: Date.now(),
// DOM Elements var __state;
$ul: $('.xblock--drag-and-drop .items', element), var __vdom = virtualDom.h(); // blank virtual DOM
$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 var init = function() {
$items: {}, // $('.xblock--drag-and-drop .items .option'), $.ajax(runtime.handlerUrl(element, 'get_data'), {
$zones: {}, // $('.xblock--drag-and-drop .target .zone'), dataType: 'json'
}).done(function(data){
setState(data);
initDroppable();
});
// jQuery UI Draggable options $(document).on('mousedown touchstart', closePopup);
options: { $(element).on('click', '.reset-button', resetExercise);
drag: { $(element).on('click', '.submit-input', submitInput);
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: { publishEvent({event_type: 'xblock.drag-and-drop-v2.loaded'});
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 var getState = function() {
_fn.tpl.init(); return __state;
};
// 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 var setState = function(new_state) {
if (_fn.data.displayLabels) { if (new_state.state.feedback) {
$('p', _fn.$zones).css('visibility', 'visible'); 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',
finish: function(final_feedback) { content: new_state.state.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(); __state = new_state;
_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); updateDOM(new_state);
$dropzone.on('dropover', handlers.drop.hover); destroyDraggable();
if (!new_state.state.finished) {
initDraggable();
}
};
$(element).on('click', '.submit-input', handlers.drag.submitInput); var updateDOM = function(state) {
var new_vdom = render(state);
var patches = virtualDom.diff(__vdom, new_vdom);
root = virtualDom.patch(root, patches);
__vdom = new_vdom;
};
$(document).on('click', function(evt) { var publishEvent = function(data) {
// 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({ $.ajax({
type: "POST", type: 'POST',
url: runtime.handlerUrl(element, "reset"), url: runtime.handlerUrl(element, 'publish_event'),
data: "{}", data: JSON.stringify(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; 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);
}
});
}; };
_fn.$popup.hide(); var initDraggable = function() {
publish_event({ $(root).find('.items .option').not('[data-drag-disabled=true]').each(function() {
event_type: 'xblock.drag-and-drop-v2.feedback.closed', try {
content: _fn.$popup.find(".popup-content").text(), $(this).draggable({
manually: true containment: '.xblock--drag-and-drop .drag-container',
cursor: 'move',
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
}); });
} }
}, });
drag: { } catch (e) {
start: function(event, ui) { // Initializing the draggable will fail if draggable was already
_fn.eventHandlers.popup.close(event, ui); // initialized. That's expected, ignore the exception.
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'), var destroyDraggable = function() {
JSON.stringify({ $(root).find('.items .option[data-drag-disabled=true]').each(function() {
val: val, try {
zone: zone, $(this).draggable('destroy');
top: $el.css('top'), } catch (e) {
left: $el.css('left') // Destroying the draggable will fail if draggable was
}), 'json').done(function(data){ // not initialized in the first place. Ignore the exception.
if (data.correct_location) { }
$el.draggable('disable'); });
};
if (data.finished) { var submitLocation = function(item_id, zone, top, left) {
_fn.finish(data.final_feedback); 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 { } else {
// Return to original position delete state.state.items[item_id];
_fn.eventHandlers.drag.reset($el);
} }
state.state.feedback = data.feedback;
if (data.feedback) { if (data.finished) {
_fn.feedback.popup(data.feedback, data.correct); state.state.finished = true;
state.feedback.finish = data.final_feedback;
} }
setState(state);
}); });
}, };
submitInput: function(evt) { var submitInput = function(evt) {
var $el = $(this).closest('li.option'); var item = $(evt.target).closest('.option');
var $input_div = $el.find('.numerical-input'); var input_div = item.find('.numerical-input');
var $input = $input_div.find('.input'); var input = input_div.find('.input');
var val = $el.data('value'); var input_value = input.val();
var item_id = item.data('value');
if (!$input.val()) { if (!input_value) {
// Don't submit if the user didn't enter anything yet. // Don't submit if the user didn't enter anything yet.
return; return;
} }
$input.prop('disabled', true); var state = getState();
$input_div.find('.submit-input').prop('disabled', true); state.state.items[item_id].input = input_value;
state.state.items[item_id].submitting_input = true;
$.post(runtime.handlerUrl(element, 'do_attempt'), setState(state);
JSON.stringify({
val: val, var url = runtime.handlerUrl(element, 'do_attempt');
input: $input.val() var data = {val: item_id, input: input_value};
}), 'json').done(function(data){ $.post(url, JSON.stringify(data), 'json').done(function(data) {
if (data.correct) { state.state.items[item_id].submitting_input = false;
$input_div.removeClass('incorrect').addClass('correct'); state.state.items[item_id].correct_input = data.correct;
} else { state.state.feedback = data.feedback;
$input_div.removeClass('correct').addClass('incorrect');
}
if (data.finished) { if (data.finished) {
_fn.finish(data.final_feedback); state.state.finished = true;
} state.feedback.finish = data.final_feedback;
if (data.feedback) {
_fn.feedback.popup(data.feedback, data.correct);
} }
setState(state);
}); });
}, };
set: function($el, top, left) { var closePopup = function(evt) {
$el.addClass('within-dropzone') var target = $(evt.target);
.css({ var popup_box = '.xblock--drag-and-drop .popup';
top: top, var close_button = '.xblock--drag-and-drop .popup .close';
left: left var submit_input_button = '.xblock--drag-and-drop .submit-input';
}) var state = getState();
.draggable('disable');
},
reset: function($el) { if (!state.state.feedback) {
$el.removeClass('within-dropzone fade') return;
.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) { if (target.is(popup_box) || target.is(submit_input_button)) {
$el.addClass('fade'); return;
} }
_fn.eventHandlers.drag.set($el, saved_entry.top, saved_entry.left); if (target.parents(popup_box).length && !target.is(close_button)) {
return;
} }
});
},
draw: function() {
var list = [],
items = _fn.data.items,
tpl = _fn.tpl.item,
img_tpl = _fn.tpl.imageItem;
items.forEach(function(item) { publishEvent({
if (item.backgroundImage.length > 0) { event_type: 'xblock.drag-and-drop-v2.feedback.closed',
list.push(img_tpl(item)); content: state.state.feedback,
} else { manually: true
list.push(tpl(item));
}
}); });
// Update DOM delete state.state.feedback;
_fn.$ul.html(list.join('')); setState(state);
};
// 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 var resetExercise = function() {
_fn.$target.html(html.join('')); $.ajax({
type: 'POST',
url: runtime.handlerUrl(element, 'reset'),
data: '{}',
success: setState
});
};
// Set variable var render = function(state) {
_fn.$zones = _fn.$target.find('.zone'); 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';
} }
},
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({ return {
event_type: "xblock.drag-and-drop-v2.feedback.opened", value: item.id,
content: str 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
};
}); });
_fn.$popup.find(".popup-content").html(str); var context = {
_fn.$popup.show(); header_html: state.title,
question_html: state.question_text,
_fn.popup_ts = Date.now(); 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,
data: null display_reset_button: state.state.finished,
zones: state.zones,
items: items
}; };
return { return DragAndDropBlock.renderView(context);
init: _fn.init,
}; };
})(jQuery);
$.ajax(runtime.handlerUrl(element, 'get_data'), {
dataType: 'json'
}).done(function(data){
dragAndDrop.init(data);
});
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"> <section class="xblock--drag-and-drop"></section>
{{ 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>
<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"> <script id="zone-element-tpl" type="text/html">
<div id="{{ id }}" class="zone" data-zone="{{ title }}" style=" <div id="{{ id }}" class="zone" data-zone="{{ title }}" style="
top:{{ y }}px; top:{{ y }}px;
......
...@@ -3,17 +3,11 @@ ...@@ -3,17 +3,11 @@
# Imports ########################################################### # Imports ###########################################################
import logging
import pkg_resources import pkg_resources
from django.template import Context, Template from django.template import Context, Template
# Globals ###########################################################
log = logging.getLogger(__name__)
# Functions ######################################################### # Functions #########################################################
def load_resource(resource_path): def load_resource(resource_path):
...@@ -23,11 +17,13 @@ def load_resource(resource_path): ...@@ -23,11 +17,13 @@ def load_resource(resource_path):
resource_content = pkg_resources.resource_string(__name__, resource_path) resource_content = pkg_resources.resource_string(__name__, resource_path)
return resource_content 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 Evaluate a template by resource path, applying the provided context
""" """
if context is None:
context = {}
template_str = load_resource(template_path) template_str = load_resource(template_path)
template = Template(template_str) template = Template(template_str)
return template.render(Context(context)) 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( ...@@ -28,7 +28,10 @@ setup(
packages=['drag_and_drop_v2'], packages=['drag_and_drop_v2'],
install_requires=[ install_requires=[
'XBlock', 'XBlock',
'xblock-utils',
'ddt'
], ],
dependency_links = ['http://github.com/edx/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={ entry_points={
'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock', 'xblock.v1': 'drag-and-drop-v2 = drag_and_drop_v2:DragAndDropBlock',
}, },
......
...@@ -89,5 +89,7 @@ ...@@ -89,5 +89,7 @@
"start": "Intro Feed", "start": "Intro Feed",
"finish": "Final 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 @@ ...@@ -68,5 +68,7 @@
"feedback": { "feedback": {
"start": "Intro Feed" "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 @@ ...@@ -68,5 +68,7 @@
"feedback": { "feedback": {
"start": "Intro Feed" "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 @@ ...@@ -89,5 +89,7 @@
"start": "Intro Feed", "start": "Intro Feed",
"finish": "Final <b>Feed</b>" "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 ...@@ -4,32 +4,21 @@ from selenium.webdriver.support.ui import WebDriverWait
from tests.utils import load_resource from tests.utils import load_resource
from workbench import scenarios from workbench import scenarios
from workbench.test.selenium_test import SeleniumTest
from xblockutils.base_test import SeleniumBaseTest
# Classes ########################################################### # Classes ###########################################################
class BaseIntegrationTest(SeleniumTest): class BaseIntegrationTest(SeleniumBaseTest):
default_css_selector = 'section.xblock--drag-and-drop'
module_name = __name__
_additional_escapes = { _additional_escapes = {
'"': "&quot;", '"': "&quot;",
"'": "&apos;" "'": "&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): def _make_scenario_xml(self, display_name, question_text, completed):
return """ return """
<vertical_demo> <vertical_demo>
...@@ -48,8 +37,8 @@ class BaseIntegrationTest(SeleniumTest): ...@@ -48,8 +37,8 @@ class BaseIntegrationTest(SeleniumTest):
self.addCleanup(scenarios.remove_scenario, identifier) self.addCleanup(scenarios.remove_scenario, identifier)
def _get_items(self): def _get_items(self):
items_container = self._page.find_element_by_css_selector('ul.items') items_container = self._page.find_element_by_css_selector('.items')
return items_container.find_elements_by_css_selector('li.option') return items_container.find_elements_by_css_selector('.option')
def _get_zones(self): def _get_zones(self):
return self._page.find_elements_by_css_selector(".drag-container .zone") return self._page.find_elements_by_css_selector(".drag-container .zone")
...@@ -57,25 +46,13 @@ class BaseIntegrationTest(SeleniumTest): ...@@ -57,25 +46,13 @@ class BaseIntegrationTest(SeleniumTest):
def _get_feedback_message(self): def _get_feedback_message(self):
return self._page.find_element_by_css_selector(".feedback .message") 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): def get_element_html(self, element):
return element.get_attribute('innerHTML').strip() return element.get_attribute('innerHTML').strip()
def get_element_classes(self, element): def get_element_classes(self, element):
return element.get_attribute('class').split() return element.get_attribute('class').split()
def scroll_to(self, y): def wait_until_html_in(self, html, elem):
self.browser.execute_script('window.scrollTo(0, {0})'.format(y))
def wait_until_contains_html(self, html, elem):
wait = WebDriverWait(elem, 2) wait = WebDriverWait(elem, 2)
wait.until(lambda e: html in e.get_attribute('innerHTML'), wait.until(lambda e: html in e.get_attribute('innerHTML'),
u"{} should be in {}".format(html, elem.get_attribute('innerHTML'))) u"{} should be in {}".format(html, elem.get_attribute('innerHTML')))
...@@ -84,4 +61,3 @@ class BaseIntegrationTest(SeleniumTest): ...@@ -84,4 +61,3 @@ class BaseIntegrationTest(SeleniumTest):
wait = WebDriverWait(elem, 2) wait = WebDriverWait(elem, 2)
wait.until(lambda e: class_name in e.get_attribute('class').split(), wait.until(lambda e: class_name in e.get_attribute('class').split(),
u"Class name {} not in {}".format(class_name, elem.get_attribute('class'))) u"Class name {} not in {}".format(class_name, elem.get_attribute('class')))
...@@ -48,15 +48,17 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -48,15 +48,17 @@ class InteractionTestFixture(BaseIntegrationTest):
scenario_xml = self._get_scenario_xml() scenario_xml = self._get_scenario_xml()
self._add_scenario(self.PAGE_ID, self.PAGE_TITLE, 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) 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): def _get_item_by_value(self, item_value):
items_container = self._page.find_element_by_css_selector('ul.items') items_container = self._page.find_element_by_css_selector('.items')
return items_container.find_elements_by_xpath("//li[@data-value='{item_id}']".format(item_id=item_value))[0] 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): 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] 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): def _get_input_div_by_value(self, item_value):
...@@ -68,7 +70,6 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -68,7 +70,6 @@ class InteractionTestFixture(BaseIntegrationTest):
element.find_element_by_class_name('input').send_keys(value) element.find_element_by_class_name('input').send_keys(value)
element.find_element_by_class_name('submit-input').click() element.find_element_by_class_name('submit-input').click()
def drag_item_to_zone(self, item_value, zone_id): def drag_item_to_zone(self, item_value, zone_id):
element = self._get_item_by_value(item_value) element = self._get_item_by_value(item_value)
target = self._get_zone_by_id(zone_id) target = self._get_zone_by_id(zone_id)
...@@ -76,11 +77,11 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -76,11 +77,11 @@ class InteractionTestFixture(BaseIntegrationTest):
action_chains.drag_and_drop(element, target).perform() action_chains.drag_and_drop(element, target).perform()
def test_item_positive_feedback_on_good_move(self): 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(): for definition in self._get_correct_item_for_zone().values():
if not definition.input: if not definition.input:
self.drag_item_to_zone(definition.item_id, definition.zone_id) 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): def test_item_positive_feedback_on_good_input(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content") feedback_popup = self._page.find_element_by_css_selector(".popup-content")
...@@ -90,7 +91,7 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -90,7 +91,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self._send_input(definition.item_id, definition.input) self._send_input(definition.item_id, definition.input)
input_div = self._get_input_div_by_value(definition.item_id) input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('correct', input_div) 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): def test_item_negative_feedback_on_bad_move(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content") feedback_popup = self._page.find_element_by_css_selector(".popup-content")
...@@ -100,7 +101,7 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -100,7 +101,7 @@ class InteractionTestFixture(BaseIntegrationTest):
if zone == definition.zone_id: if zone == definition.zone_id:
continue continue
self.drag_item_to_zone(definition.item_id, zone) 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): def test_item_positive_feedback_on_bad_input(self):
feedback_popup = self._page.find_element_by_css_selector(".popup-content") feedback_popup = self._page.find_element_by_css_selector(".popup-content")
...@@ -110,7 +111,7 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -110,7 +111,7 @@ class InteractionTestFixture(BaseIntegrationTest):
self._send_input(definition.item_id, '1999999') self._send_input(definition.item_id, '1999999')
input_div = self._get_input_div_by_value(definition.item_id) input_div = self._get_input_div_by_value(definition.item_id)
self.wait_until_has_class('incorrect', input_div) 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): def test_final_feedback_and_reset(self):
feedback_message = self._get_feedback_message() feedback_message = self._get_feedback_message()
...@@ -128,16 +129,13 @@ class InteractionTestFixture(BaseIntegrationTest): ...@@ -128,16 +129,13 @@ class InteractionTestFixture(BaseIntegrationTest):
input_div = self._get_input_div_by_value(item_key) input_div = self._get_input_div_by_value(item_key)
self.wait_until_has_class('correct', input_div) 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 reset = self._page.find_element_by_css_selector('.reset-button')
# 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.click() 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() locations_after_reset = get_locations()
for item_key in items.keys(): for item_key in items.keys():
......
from nose_parameterized import parameterized from ddt import ddt, unpack, data
from tests.integration.test_base import BaseIntegrationTest from tests.integration.test_base import BaseIntegrationTest
from workbench import scenarios from workbench import scenarios
@ddt
class TestDragAndDropTitleAndQuestion(BaseIntegrationTest): class TestDragAndDropTitleAndQuestion(BaseIntegrationTest):
@parameterized.expand([ @unpack
@data(
('plain1', 'title1', 'question1'), ('plain1', 'title1', 'question1'),
('plain2', 'title2', 'question2'), ('plain2', 'title2', 'question2'),
('html1', 'title with <i>HTML</i>', 'Question with <i>HTML</i>'), ('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>'), ('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): def test_title_and_question_parameters(self, _, display_name, question_text):
const_page_name = 'Test block parameters' const_page_name = 'Test block parameters'
const_page_id = 'test_block_title' 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 ...@@ -7,6 +7,7 @@ from webob import Request
from mock import Mock from mock import Mock
from workbench.runtime import WorkbenchRuntime from workbench.runtime import WorkbenchRuntime
from xblock.fields import ScopeIds
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
from nose.tools import ( from nose.tools import (
...@@ -33,10 +34,14 @@ def make_request(body, method='POST'): ...@@ -33,10 +34,14 @@ def make_request(body, method='POST'):
def make_block(): def make_block():
runtime = WorkbenchRuntime() block_type = 'drag_and_drop_v2'
key_store = DictKeyValueStore() key_store = DictKeyValueStore()
db_model = KvsFieldData(key_store) field_data = KvsFieldData(key_store)
return drag_and_drop_v2.DragAndDropBlock(runtime, db_model, Mock()) 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(): def test_templates_contents():
...@@ -46,17 +51,9 @@ def test_templates_contents(): ...@@ -46,17 +51,9 @@ def test_templates_contents():
block.question_text = "Question Drag & Drop" block.question_text = "Question Drag & Drop"
block.weight = 5 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">', assert_in('<section class="xblock--drag-and-drop">',
student_fragment.content) 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(): def test_studio_submit():
block = make_block() block = make_block()
...@@ -176,7 +173,7 @@ class BaseDragAndDropAjaxFixture(object): ...@@ -176,7 +173,7 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response() expected = self.get_data_response()
expected["state"] = { expected["state"] = {
"items": { "items": {
"1": {"top": "22px", "left": "222px", "correct_input": False} "1": {"top": "22px", "left": "222px", "absolute": True, "correct_input": False}
}, },
"finished": False "finished": False
} }
...@@ -196,7 +193,8 @@ class BaseDragAndDropAjaxFixture(object): ...@@ -196,7 +193,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response() expected = self.get_data_response()
expected["state"] = { expected["state"] = {
"items": { "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 "finished": False
} }
...@@ -216,7 +214,8 @@ class BaseDragAndDropAjaxFixture(object): ...@@ -216,7 +214,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response() expected = self.get_data_response()
expected["state"] = { expected["state"] = {
"items": { "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 "finished": False
} }
...@@ -255,7 +254,8 @@ class BaseDragAndDropAjaxFixture(object): ...@@ -255,7 +254,8 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response() expected = self.get_data_response()
expected["state"] = { expected["state"] = {
"items": { "items": {
"0": {"top": "11px", "left": "111px", "correct_input": True} "0": {"top": "11px", "left": "111px", "absolute": True,
"correct_input": True}
}, },
"finished": False "finished": False
} }
...@@ -277,8 +277,9 @@ class BaseDragAndDropAjaxFixture(object): ...@@ -277,8 +277,9 @@ class BaseDragAndDropAjaxFixture(object):
expected = self.get_data_response() expected = self.get_data_response()
expected["state"] = { expected["state"] = {
"items": { "items": {
"0": {"top": "11px", "left": "111px", "correct_input": True}, "0": {"top": "11px", "left": "111px", "absolute": True, "correct_input": True},
"1": {"top": "22px", "left": "222px", "input": "99", "correct_input": True} "1": {"top": "22px", "left": "222px", "absolute": True, "input": "99",
"correct_input": True}
}, },
"finished": True "finished": True
} }
...@@ -336,8 +337,8 @@ def test_ajax_solve_and_reset(): ...@@ -336,8 +337,8 @@ def test_ajax_solve_and_reset():
block.handle('do_attempt', make_request(data)) block.handle('do_attempt', make_request(data))
assert_true(block.completed) assert_true(block.completed)
assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px"}, assert_equals(block.item_state, {'0': {"top": "11px", "left": "111px", "absolute": True},
'1': {"top": "22px", "left": "222px"}}) '1': {"top": "22px", "left": "222px", "absolute": True}})
block.handle('reset', make_request("{}")) 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