Commit 1fe66eff by Filippo Valsorda

Add server-side checking, feedback and saved state

parent b42f4f5e
...@@ -6,12 +6,13 @@ ...@@ -6,12 +6,13 @@
import logging import logging
import json import json
import webob import webob
import copy
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, Dict from xblock.fields import Scope, String, Dict
from xblock.fragment import Fragment from xblock.fragment import Fragment
from .utils import render_template, load_resource from .utils import render_template
# Globals ########################################################### # Globals ###########################################################
...@@ -44,13 +45,22 @@ class DragAndDropBlock(XBlock): ...@@ -44,13 +45,22 @@ class DragAndDropBlock(XBlock):
help="JSON spec as generated by the builder", help="JSON spec as generated by the builder",
scope=Scope.content, scope=Scope.content,
default={ default={
'feedback': {}, 'feedback': {
'start': '',
'finish': ''
},
'items': [], 'items': [],
'zones': [], 'zones': [],
'targetImg': None 'targetImg': None
} }
) )
item_state = Dict(
help="How the student has interacted with the problem",
scope=Scope.user_state,
default={}
)
def student_view(self, context): def student_view(self, context):
""" """
Player view, displayed to the student Player view, displayed to the student
...@@ -121,4 +131,42 @@ class DragAndDropBlock(XBlock): ...@@ -121,4 +131,42 @@ class DragAndDropBlock(XBlock):
@XBlock.handler @XBlock.handler
def get_data(self, request, suffix=''): def get_data(self, request, suffix=''):
return webob.response.Response(body=json.dumps(self.data)) data = copy.deepcopy(self.data)
for item in data['items']:
# Strip answers
del item['feedback']
del item['zone']
tot_items = sum(1 for i in self.data['items'] if i['zone'] != 'none')
if len(self.item_state) != tot_items:
del data['feedback']['finish']
data['state'] = self.item_state
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')
if item['zone'] == attempt['zone']:
self.item_state[item['id']] = (attempt['top'], attempt['left'])
if len(self.item_state) == tot_items:
final_feedback = self.data['feedback']['finish']
else:
final_feedback = None
return {
'correct': True,
'final_feedback': final_feedback,
'feedback': item['feedback']['correct']
}
else:
return {
'correct': False,
'final_feedback': None,
'feedback': item['feedback']['incorrect']
}
...@@ -52,7 +52,8 @@ ...@@ -52,7 +52,8 @@
position: relative; position: relative;
float: left; float: left;
display: inline; display: inline;
z-index: 100; /* Some versions of the drag and drop library try to fiddle with this */
z-index: 10 !important;
margin-bottom: 5px; margin-bottom: 5px;
padding: 10px; padding: 10px;
} }
......
...@@ -64,9 +64,6 @@ function DragAndDropBlock(runtime, element) { ...@@ -64,9 +64,6 @@ function DragAndDropBlock(runtime, element) {
_fn.items.draw(); _fn.items.draw();
_fn.zones.draw(); _fn.zones.draw();
// Load welcome feedback
_fn.feedback.set(_fn.data.feedback.start);
// Init drag and drop plugin // Init drag and drop plugin
_fn.$items.draggable(_fn.options.drag); _fn.$items.draggable(_fn.options.drag);
_fn.$zones.droppable(_fn.options.drop); _fn.$zones.droppable(_fn.options.drop);
...@@ -74,20 +71,26 @@ function DragAndDropBlock(runtime, element) { ...@@ -74,20 +71,26 @@ function DragAndDropBlock(runtime, element) {
// Init click handlers // Init click handlers
_fn.clickHandlers.init(_fn.$items, _fn.$zones); _fn.clickHandlers.init(_fn.$items, _fn.$zones);
// Get count of all active items // Position the already correct items
_fn.items.init(); _fn.items.init();
// Load welcome or final feedback
if (_fn.data.feedback.finish)
_fn.finish(_fn.data.feedback.finish);
else
_fn.feedback.set(_fn.data.feedback.start);
// Set the target image // Set the target image
if (_fn.data.targetImg) if (_fn.data.targetImg)
_fn.$target.css('background', 'url(' + _fn.data.targetImg + ') no-repeat'); _fn.$target.css('background', 'url(' + _fn.data.targetImg + ') no-repeat');
}, },
finish: function() { finish: function(final_feedback) {
// Disable any decoy items // Disable any decoy items
_fn.$items.draggable('disable'); _fn.$items.draggable('disable');
// Show final feedback // Show final feedback
_fn.feedback.set(_fn.data.feedback.finish); _fn.feedback.set(final_feedback);
}, },
clickHandlers: { clickHandlers: {
...@@ -111,21 +114,44 @@ function DragAndDropBlock(runtime, element) { ...@@ -111,21 +114,44 @@ function DragAndDropBlock(runtime, element) {
val = $el.data('value'), val = $el.data('value'),
zone = $el.data('zone') || null; zone = $el.data('zone') || null;
if ($el.hasClass('within-dropzone') && _fn.test.match(val, zone)) { if (!$el.hasClass('within-dropzone')) {
// Return to original position
_fn.clickHandlers.drag.reset($el);
return;
}
$.post(runtime.handlerUrl(element, 'do_attempt'),
JSON.stringify({
val: val,
zone: zone,
top: $el.css('top'),
left: $el.css('left')
})).done(function(data){
if (data.correct) {
$el.removeClass('hover') $el.removeClass('hover')
.draggable('disable'); .draggable('disable');
_fn.test.completed++; if (data.final_feedback) {
_fn.feedback.popup(_fn.feedback.get(val, true), true); _fn.finish(data.final_feedback);
if (_fn.items.allSubmitted()) {
_fn.finish();
} }
} else { } else {
// Return to original position // Return to original position
_fn.clickHandlers.drag.reset($el); _fn.clickHandlers.drag.reset($el);
_fn.feedback.popup(_fn.feedback.get(val, false), false);
} }
if (data.feedback) {
_fn.feedback.popup(data.feedback, data.correct);
}
});
},
set: function($el, top, left) {
$el.addClass('within-dropzone fade')
.css({
top: top,
left: left
})
.draggable('disable');
}, },
reset: function($el) { reset: function($el) {
...@@ -154,23 +180,15 @@ function DragAndDropBlock(runtime, element) { ...@@ -154,23 +180,15 @@ function DragAndDropBlock(runtime, element) {
}, },
items: { items: {
count: 0,
init: function() { init: function() {
var items = _fn.data.items, _fn.$items.each(function (){
i, var $el = $(this),
len = items.length, saved_entry = _fn.data.state[$el.data('value')];
total = 0; if (saved_entry) {
_fn.clickHandlers.drag.set($el,
for (i=0; i<len; i++) { saved_entry[0], saved_entry[1]);
if (items[i].zone !== 'none') {
total++;
} }
} });
_fn.items.count = total;
},
allSubmitted: function() {
return _fn.test.completed === _fn.items.count;
}, },
draw: function() { draw: function() {
var list = [], var list = [],
...@@ -215,31 +233,10 @@ function DragAndDropBlock(runtime, element) { ...@@ -215,31 +233,10 @@ function DragAndDropBlock(runtime, element) {
}, },
test: { test: {
completed: 0, completed: 0
match: function(id, zone) {
var item = _.findWhere(_fn.data.items, { id: id });
return item.zone === zone;
}
}, },
feedback: { feedback: {
// Returns string based on user's answer
get: function(id, boo) {
var item,
type = boo ? 'correct' : 'incorrect';
// Null loses its string-ness
if (id === null) {
id = 'null';
}
// Get object from data.items that matches val
item = _.findWhere(_fn.data.items, { id: id });
return item.feedback[type];
},
// Update DOM with feedback // Update DOM with feedback
set: function(str) { set: function(str) {
return _fn.$feedback.html(str); return _fn.$feedback.html(str);
......
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