-
Notifications
You must be signed in to change notification settings - Fork 3
Initial implementation #1
Changes from 6 commits
6cb67fc
9a9a324
dd06d81
aa7c134
c19b0e4
6c1b897
6b87e0a
c71e96b
a838256
273232b
06d0cbb
fbc4ea4
06eacfc
deac8d0
bc15783
a67ccc1
918ed51
b574ec3
708b2cc
f19ab78
c3a098d
d2fe045
d2d0e84
224391a
602d021
9e6e001
dcf0840
792d4ba
d2a9f6e
0d4a06a
645115f
c03551d
461371b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
__pycache__/ | ||
*.py[cod] | ||
vectordraw_xblock.egg-info/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
"""Setup for vectordraw XBlock.""" | ||
|
||
import os | ||
from setuptools import setup | ||
|
||
|
||
def package_data(pkg, roots): | ||
"""Generic function to find package_data. | ||
|
||
All of the files under each of the `roots` will be declared as package | ||
data for package `pkg`. | ||
|
||
""" | ||
data = [] | ||
for root in roots: | ||
for dirname, _, files in os.walk(os.path.join(pkg, root)): | ||
for fname in files: | ||
data.append(os.path.relpath(os.path.join(dirname, fname), pkg)) | ||
|
||
return {pkg: data} | ||
|
||
|
||
setup( | ||
name='vectordraw-xblock', | ||
version='0.1', | ||
description='vectordraw XBlock', # TODO: write a better description. | ||
packages=[ | ||
'vectordraw', | ||
], | ||
install_requires=[ | ||
'XBlock', | ||
], | ||
entry_points={ | ||
'xblock.v1': [ | ||
'vectordraw = vectordraw:VectorDrawXBlock', | ||
] | ||
}, | ||
package_data=package_data("vectordraw", ["static", "public"]), | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .vectordraw import VectorDrawXBlock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like 'expected' is a required key, so use |
||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like 'expected' is a required key, so use check['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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks to me like this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @singingwolfboy You're right about the fact that the |
||
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed this hack I introduced when adding points as a problem type. It's main purpose was to stay backwards-compatible with existing external graders. We might now be able to simply pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw, another way to do this would be to declare all checks with a |
||
result = check_fn(*args) | ||
if result: | ||
return {'ok': False, 'msg': result} | ||
return {'ok': True, 'msg': self.success_message} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This return type seems wrong to me. Python allows functions to return multiple values. If you want the function to report both whether the student's answer was correct or not, and a message to report to the student, you can just return those two values, rather than combining them into a dictionary. I also don't like the word There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @singingwolfboy Definitely agree on |
||
|
||
def cfn(self, e, ans): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this function is needed for the XBlock version – it looks quite JSInput-specific. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @smarnach Ah, yes - missed that somehow when I was cleaning up. Thanks! |
||
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()} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/* CSS for VectorDrawXBlock */ | ||
|
||
.vectordraw_block, | ||
.vectordraw_block #vectordraw { | ||
display: inline-block; | ||
} | ||
|
||
.vectordraw_block .vectordraw-description, | ||
.vectordraw_block #vectordraw, | ||
.vectordraw_block .vectordraw-status { | ||
margin-bottom: 1.5em; | ||
} | ||
|
||
.vectordraw_block .jxgboard { | ||
float: left; | ||
border: 2px solid #1f628d; | ||
} | ||
|
||
.vectordraw_block .jxgboard .JXGtext { | ||
pointer-events: none; /* prevents cursor from turning into caret when over a label */ | ||
} | ||
|
||
.vectordraw_block .menu { | ||
float: left; | ||
width: 160px; | ||
margin-left: 15px; | ||
} | ||
|
||
.vectordraw_block .menu .controls { | ||
margin-bottom: 20px; | ||
font-size: 0; | ||
} | ||
|
||
.vectordraw_block .menu .controls select { | ||
width: 160px; | ||
margin-bottom: 8px; | ||
font-size: 18px; | ||
} | ||
|
||
.vectordraw_block .menu .controls button { | ||
display: inline-block; | ||
background-color: #3498db; | ||
border-radius: 5px; | ||
box-shadow: 0 1px 3px #666; | ||
color: #1f628d; | ||
font-size: 14px; | ||
padding: 5px 10px 5px 10px; | ||
margin: 4px 0; | ||
border: 2px solid #1f628d; | ||
text-decoration: none; | ||
width: 160px; | ||
} | ||
|
||
.vectordraw_block .menu .controls button:hover { | ||
background: #3cb0fd; | ||
background-image: -webkit-linear-gradient(top, #3cb0fd, #3498db); | ||
background-image: -moz-linear-gradient(top, #3cb0fd, #3498db); | ||
background-image: -ms-linear-gradient(top, #3cb0fd, #3498db); | ||
background-image: -o-linear-gradient(top, #3cb0fd, #3498db); | ||
background-image: linear-gradient(to bottom, #3cb0fd, #3498db); | ||
text-decoration: none; | ||
} | ||
|
||
vectordraw_block .menu .controls button.undo, | ||
vectordraw_block .menu .controls button.redo { | ||
width: 78px; | ||
} | ||
|
||
.vectordraw_block .menu .controls button.undo { | ||
margin-right: 4px; | ||
} | ||
|
||
.vectordraw_block .menu .vector-properties { | ||
padding: 10px; | ||
border: 1px solid #1f628d; | ||
font-size: 16px; | ||
line-height: 1.25; | ||
} | ||
|
||
.vectordraw_block .menu .vector-properties h3 { | ||
font-size: 16px; | ||
margin: 0 0 5px; | ||
} | ||
|
||
.vectordraw_block .menu .vector-properties .vector-prop-bold { | ||
font-weight: bold; | ||
} | ||
|
||
.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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function has a great similarity to check_tail. Is there a way to share code between the two?