Commit aa7c1340 by Tim Krones

Add grading functionality.

parent dd06d817
import inspect
import json
import logging
import math
log = logging.getLogger(__name__)
## Built-in check functions
def _errmsg(default_message, check, vectors):
template = check.get('errmsg', default_message)
vec = vectors[check['vector']]
return template.format(name=vec.name,
tail_x=vec.tail.x,
tail_y=vec.tail.y,
tip_x=vec.tip.x,
tip_y=vec.tip.y,
length=vec.length,
angle=vec.angle)
def _errmsg_point(default_message, check, point):
template = check.get('errmsg', default_message)
return template.format(name=check['point'], x=point.x, y=point.y)
def check_presence(check, vectors):
if check['vector'] not in vectors:
errmsg = check.get('errmsg', 'You need to use the {name} vector.')
return errmsg.format(name=check['vector'])
def check_tail(check, vectors):
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
dist = math.hypot(expected[0] - vec.tail.x, expected[1] - vec.tail.y)
if dist > tolerance:
return _errmsg('Vector {name} does not start at correct point.', check, vectors)
def check_tip(check, vectors):
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
dist = math.hypot(expected[0] - vec.tip.x, expected[1] - vec.tip.y)
if dist > tolerance:
return _errmsg('Vector {name} does not end at correct point.', check, vectors)
def _check_coordinate(check, coord):
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
return abs(expected - coord) > tolerance
def check_tail_x(check, vectors):
vec = vectors[check['vector']]
if _check_coordinate(check, vec.tail.x):
return _errmsg('Vector {name} does not start at correct point.', check, vectors)
def check_tail_y(check, vectors):
vec = vectors[check['vector']]
if _check_coordinate(check, vec.tail.y):
return _errmsg('Vector {name} does not start at correct point.', check, vectors)
def check_tip_x(check, vectors):
vec = vectors[check['vector']]
if _check_coordinate(check, vec.tip.x):
return _errmsg('Vector {name} does not end at correct point.', check, vectors)
def check_tip_y(check, vectors):
vec = vectors[check['vector']]
if _check_coordinate(check, vec.tip.y):
return _errmsg('Vector {name} does not end at correct point.', check, vectors)
def _coord_delta(expected, actual):
if expected == '_':
return 0
else:
return expected - actual
def _coords_within_tolerance(vec, expected, tolerance):
for expected_coords, vec_coords in ([expected[0], vec.tail], [expected[1], vec.tip]):
delta_x = _coord_delta(expected_coords[0], vec_coords.x)
delta_y = _coord_delta(expected_coords[1], vec_coords.y)
if math.hypot(delta_x, delta_y) > tolerance:
return False
return True
def check_coords(check, vectors):
vec = vectors[check['vector']]
expected = check['expected']
tolerance = check.get('tolerance', 1.0)
if not _coords_within_tolerance(vec, expected, tolerance):
return _errmsg('Vector {name} coordinates are not correct.', check, vectors)
def check_segment_coords(check, vectors):
vec = vectors[check['vector']]
expected = check['expected']
tolerance = check.get('tolerance', 1.0)
if not (_coords_within_tolerance(vec, expected, tolerance) or
_coords_within_tolerance(vec.opposite(), expected, tolerance)):
return _errmsg('Segment {name} coordinates are not correct.', check, vectors)
def check_length(check, vectors):
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
if abs(vec.length - check['expected']) > tolerance:
return _errmsg('The length of {name} is incorrect. Your length: {length:.1f}', check, vectors)
def _angle_within_tolerance(vec, expected, tolerance):
# Calculate angle between vec and identity vector with expected angle
# using the formula:
# angle = acos((A . B) / len(A)*len(B))
x = vec.tip.x - vec.tail.x
y = vec.tip.y - vec.tail.y
dot_product = x * math.cos(expected) + y * math.sin(expected)
angle = math.degrees(math.acos(dot_product / vec.length))
return abs(angle) <= tolerance
def check_angle(check, vectors):
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 2.0)
expected = math.radians(check['expected'])
if not _angle_within_tolerance(vec, expected, tolerance):
return _errmsg('The angle of {name} is incorrect. Your angle: {angle:.1f}', check, vectors)
def check_segment_angle(check, vectors):
# Segments are not directed, so we must check the angle between the segment and
# the vector that represents it, as well as its opposite vector.
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 2.0)
expected = math.radians(check['expected'])
if not (_angle_within_tolerance(vec, expected, tolerance) or
_angle_within_tolerance(vec.opposite(), expected, tolerance)):
return _errmsg('The angle of {name} is incorrect. Your angle: {angle:.1f}', check, vectors)
def _dist_line_point(line, point):
# Return the distance between the given line and point. The line is passed in as a Vector
# instance, the point as a Point instance.
direction_x = line.tip.x - line.tail.x
direction_y = line.tip.y - line.tail.y
determinant = (point.x - line.tail.x) * direction_y - (point.y - line.tail.y) * direction_x
return abs(determinant) / math.hypot(direction_x, direction_y)
def check_points_on_line(check, vectors):
line = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
points = check.get('expected')
for point in points:
point = Point(point[0], point[1])
if _dist_line_point(line, point) > tolerance:
return _errmsg('The line {name} does not pass through the correct points.', check, vectors)
def check_point_coords(check, points):
point = points[check['point']]
tolerance = check.get('tolerance', 1.0)
expected = check.get('expected')
dist = math.hypot(expected[0] - point.x, expected[1] - point.y)
if dist > tolerance:
return _errmsg_point('Point {name} is not at the correct location.', check, point)
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
class Vector(object):
def __init__(self, name, x1, y1, x2, y2):
self.name = name
self.tail = Point(x1, y1)
self.tip = Point(x2, y2)
self.length = math.hypot(x2 - x1, y2 - y1)
angle = math.degrees(math.atan2(y2 - y1, x2 - x1))
if angle < 0:
angle += 360
self.angle = angle
def opposite(self):
return Vector(self.name, self.tip.x, self.tip.y, self.tail.x, self.tail.y)
class Grader(object):
check_registry = {
'presence': check_presence,
'tail': check_tail,
'tip': check_tip,
'tail_x': check_tail_x,
'tail_y': check_tail_y,
'tip_x': check_tip_x,
'tip_y': check_tip_y,
'coords': check_coords,
'length': check_length,
'angle': check_angle,
'segment_angle': check_segment_angle,
'segment_coords': check_segment_coords,
'points_on_line': check_points_on_line,
'point_coords': check_point_coords,
}
def __init__(self, success_message='Test passed', custom_checks=None):
self.success_message = success_message
if custom_checks:
self.check_registry.update(custom_checks)
def grade(self, answer):
check_data = dict(
vectors=self._get_vectors(answer),
points=self._get_points(answer),
)
for check in answer['checks']:
check_data['check'] = check
check_fn = self.check_registry[check['check']]
args = [check_data[arg] for arg in inspect.getargspec(check_fn).args]
result = check_fn(*args)
if result:
return {'ok': False, 'msg': result}
return {'ok': True, 'msg': self.success_message}
def cfn(self, e, ans):
answer = json.loads(json.loads(ans)['answer'])
return self.grade(answer)
def _get_vectors(self, answer):
vectors = {}
for name, props in answer['vectors'].iteritems():
tail = props['tail']
tip = props['tip']
vectors[name] = Vector(name, tail[0], tail[1], tip[0], tip[1])
return vectors
def _get_points(self, answer):
return {name: Point(*coords) for name, coords in answer['points'].iteritems()}
/* CSS for VectorDrawXBlock */
.vectordraw_block {
.vectordraw_block,
.vectordraw_block #vectordraw {
display: inline-block;
}
.vectordraw_block .vectordraw-description {
.vectordraw_block .vectordraw-description,
.vectordraw_block #vectordraw,
.vectordraw_block .vectordraw-status {
margin-bottom: 1.5em;
}
......@@ -86,3 +89,25 @@ vectordraw_block .menu .controls button.redo {
.vectordraw_block .menu .vector-prop-slope {
display: none;
}
.vectordraw_block .action button {
height: 40px;
margin-right: 10px;
font-weight: 600;
text-transform: uppercase;
}
.vectordraw_block .vectordraw-status {
display: inline-block;
width: 100%;
}
.vectordraw_block .checkmark-correct {
font-size: 22pt;
color: #629b2b;
}
.vectordraw_block .checkmark-incorrect {
font-size: 22pt;
color: #ff0000;
}
......@@ -10,4 +10,16 @@
<div id="vectordraw" />
<div class="vectordraw-status">
<span class="correctness icon-2x"></span>
<div class="status-message"></div>
</div>
<div class="action">
<button class="check">
<span class="check-label">Check</span>
<span class="sr"> your answer</span>
</button>
</div>
</div>
......@@ -545,11 +545,75 @@ function VectorDrawXBlock(runtime, element, init_args) {
this.board.update();
};
var checkHandlerUrl = runtime.handlerUrl(element, 'check_answers');
var checkXHR;
function getInput(vectordraw) {
var input = vectordraw.getState();
// Transform the expected_result setting into a list of checks.
var checks = [];
_.each(vectordraw.settings.expected_result, function(answer, name) {
var presence_check = {vector: name, check: 'presence'};
if ('presence_errmsg' in answer) {
presence_check.errmsg = answer.presence_errmsg;
}
checks.push(presence_check);
[
'tail', 'tail_x', 'tail_y', 'tip', 'tip_x', 'tip_y', 'coords',
'length', 'angle', 'segment_angle', 'segment_coords', 'points_on_line'
].forEach(function(prop) {
if (prop in answer) {
var check = {vector: name, check: prop, expected: answer[prop]};
if (prop + '_tolerance' in answer) {
check.tolerance = answer[prop + '_tolerance'];
}
if (prop + '_errmsg' in answer) {
check.errmsg = answer[prop + '_errmsg'];
}
checks.push(check);
}
});
});
input.checks = checks.concat(vectordraw.settings.custom_checks);
return input;
}
function updateStatus(data) {
var correctness = $('.correctness', element);
if (data.result.ok) {
correctness.removeClass('checkmark-incorrect fa fa-times');
correctness.addClass('checkmark-correct fa fa-check');
} else {
correctness.removeClass('checkmark-correct fa fa-check');
correctness.addClass('checkmark-incorrect fa fa-times');
}
$('.status-message', element).text(data.result.msg);
}
function checkAnswers(vectordraw) {
if (checkXHR) {
checkXHR.abort();
}
var state = getInput(vectordraw);
checkXHR = $.post(checkHandlerUrl, JSON.stringify(state))
.success(function(response) {
console.log(JSON.stringify(response));
updateStatus(response);
});
}
$(function ($) {
/* Here's where you'd do things on page load. */
var vectordraw = new VectorDraw('vectordraw', init_args);
$('.action .check', element).on('click', function(e) { checkAnswers(vectordraw); });
});
}
"""TO-DO: Write a description of what this XBlock is."""
import json
import logging
import pkg_resources
from xblock.core import XBlock
from xblock.fields import Scope, Boolean, Integer, String
from xblock.fields import Scope, Boolean, Float, Integer, String
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
from .grader import Grader
loader = ResourceLoader(__name__)
log = logging.getLogger(__name__)
class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
"""
......@@ -179,6 +184,13 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
scope=Scope.content
)
weight = Float(
display_name="Weight",
default=1,
scope=Scope.settings,
enforce_type=True
)
editable_fields = (
'display_name',
'description',
......@@ -200,6 +212,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'custom_checks'
)
has_score = True
@property
def background(self):
return {
......@@ -216,6 +230,10 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
def points_json(self):
return json.loads(self.points)
@property
def expected_result_json(self):
return json.loads(self.expected_result)
def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string(__name__, path)
......@@ -230,6 +248,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
context['self'] = self
fragment = Fragment()
fragment.add_content(loader.render_template('static/html/vectordraw.html', context))
fragment.add_css_url("//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css")
fragment.add_css(self.resource_string('static/css/vectordraw.css'))
fragment.add_javascript_url("//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js")
fragment.add_javascript(self.resource_string("static/js/src/vectordraw.js"))
......@@ -248,6 +267,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'background': self.background,
'vectors': self.vectors_json,
'points': self.points_json,
'expected_result': self.expected_result_json
}
)
return fragment
......@@ -255,15 +275,20 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
# TO-DO: change this handler to perform your own actions. You may need more
# than one handler, or you may not need any handlers at all.
@XBlock.json_handler
def increment_count(self, data, suffix=''):
def check_answers(self, data, suffix=''):
"""
An example handler, which increments the data.
Check student's answers
"""
# Just to show data coming in...
assert data['hello'] == 'world'
self.count += 1
return {"count": self.count}
grader = Grader()
result = grader.grade(data)
# Publish grade data
score = 1 if result["ok"] else 0
self.runtime.publish(self, 'grade', dict(value=score, max_value=1))
return {
"message": "Success!",
"data": data,
"result": result,
}
# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
......
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