Commit 273232bc by Tim Krones

Fix PEP8 and pylint issues.

parent a8382565
"""
Top-level package for Vector Drawing XBlock.
See vectordraw.vectordraw for more information.
"""
from .vectordraw import VectorDrawXBlock
"""
This module contains grading logic for Vector Drawing exercises.
"""
# pylint: disable=invalid-name
import inspect
import json
import logging
import math
log = logging.getLogger(__name__)
log = logging.getLogger(__name__) # pylint: disable=invalid-name
## Built-in check functions
# Built-in check functions
def _errmsg(default_message, check, vectors):
"""
Return error message for `check` targeting a vector from `vectors`.
If `check` does not define a custom error message, fall back on `default_message`.
"""
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)
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):
"""
Return error message for `check` targeting `point`.
If `check` does not define a custom error message, fall back on `default_message`.
"""
template = check.get('errmsg', default_message)
return template.format(name=check['point'], x=point.x, y=point.y)
def check_presence(check, vectors):
"""
Check if `vectors` contains vector targeted by `check`.
"""
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):
"""
Check if tail of vector targeted by `check` is in correct position.
"""
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
......@@ -37,7 +63,11 @@ def check_tail(check, vectors):
if dist > tolerance:
return _errmsg('Vector {name} does not start at correct point.', check, vectors)
def check_tip(check, vectors):
"""
Check if tip of vector targeted by `check` is in correct position.
"""
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
......@@ -45,38 +75,66 @@ def check_tip(check, vectors):
if dist > tolerance:
return _errmsg('Vector {name} does not end at correct point.', check, vectors)
def _check_coordinate(check, coord):
"""
Check `coord` against expected value.
"""
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
return abs(expected - coord) > tolerance
def check_tail_x(check, vectors):
"""
Check if x position of tail of vector targeted by `check` is correct.
"""
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):
"""
Check if y position of tail of vector targeted by `check` is correct.
"""
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):
"""
Check if x position of tip of vector targeted by `check` is correct.
"""
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):
"""
Check if y position of tip of vector targeted by `check` is correct.
"""
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):
"""
Return distance between `expected` and `actual` coordinates.
"""
if expected == '_':
return 0
else:
return expected - actual
def _coords_within_tolerance(vec, expected, tolerance):
"""
Check if distance between coordinates of `vec` and `expected` coordinates is within `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)
......@@ -84,14 +142,22 @@ def _coords_within_tolerance(vec, expected, tolerance):
return False
return True
def check_coords(check, vectors):
"""
Check if coordinates of vector targeted by `check` are in correct position.
"""
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):
"""
Check if coordinates of segment targeted by `check` are in correct position.
"""
vec = vectors[check['vector']]
expected = check['expected']
tolerance = check.get('tolerance', 1.0)
......@@ -99,13 +165,23 @@ def check_segment_coords(check, vectors):
_coords_within_tolerance(vec.opposite(), expected, tolerance)):
return _errmsg('Segment {name} coordinates are not correct.', check, vectors)
def check_length(check, vectors):
"""
Check if length of vector targeted by `check` is correct.
"""
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)
return _errmsg(
'The length of {name} is incorrect. Your length: {length:.1f}', check, vectors
)
def _angle_within_tolerance(vec, expected, tolerance):
"""
Check if difference between angle of `vec` and `expected` angle is within `tolerance`.
"""
# Calculate angle between vec and identity vector with expected angle
# using the formula:
# angle = acos((A . B) / len(A)*len(B))
......@@ -115,14 +191,22 @@ def _angle_within_tolerance(vec, expected, tolerance):
angle = math.degrees(math.acos(dot_product / vec.length))
return abs(angle) <= tolerance
def check_angle(check, vectors):
"""
Check if angle of vector targeted by `check` is correct.
"""
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):
"""
Check if angle of segment targeted by `check` is correct.
"""
# 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']]
......@@ -132,24 +216,38 @@ def check_segment_angle(check, vectors):
_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.
"""
Return distance between `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):
"""
Check if line targeted by `check` passes through correct points.
"""
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)
return _errmsg(
'The line {name} does not pass through the correct points.', check, vectors
)
def check_point_coords(check, points):
"""
Check if coordinates of point targeted by `check` are correct.
"""
point = points[check['point']]
tolerance = check.get('tolerance', 1.0)
expected = check.get('expected')
......@@ -157,13 +255,17 @@ def check_point_coords(check, points):
if dist > tolerance:
return _errmsg_point('Point {name} is not at the correct location.', check, point)
class Point(object):
""" Represents a single point on the vector drawing board. """
def __init__(self, x, y):
self.x = x
self.y = y
class Vector(object):
def __init__(self, name, x1, y1, x2, y2):
""" Represents a single vector on the vector drawing board. """
def __init__(self, name, x1, y1, x2, y2): # pylint: disable=too-many-arguments
self.name = name
self.tail = Point(x1, y1)
self.tip = Point(x2, y2)
......@@ -174,9 +276,16 @@ class Vector(object):
self.angle = angle
def opposite(self):
"""
Return new vector with tip and tail swapped.
"""
return Vector(self.name, self.tip.x, self.tip.y, self.tail.x, self.tail.y)
class Grader(object):
"""
Implements grading logic for student answers to Vector Drawing exercises.
"""
check_registry = {
'presence': check_presence,
'tail': check_tail,
......@@ -200,6 +309,11 @@ class Grader(object):
self.check_registry.update(custom_checks)
def grade(self, answer):
"""
Check correctness of `answer` by running checks defined for it one by one.
Short-circuit as soon as a single check fails.
"""
check_data = dict(
vectors=self._get_vectors(answer),
points=self._get_points(answer),
......@@ -213,7 +327,10 @@ class Grader(object):
return {'ok': False, 'msg': result}
return {'ok': True, 'msg': self.success_message}
def _get_vectors(self, answer):
def _get_vectors(self, answer): # pylint: disable=no-self-use
"""
Turn vector info in `answer` into a dictionary of Vector objects.
"""
vectors = {}
for name, props in answer['vectors'].iteritems():
tail = props['tail']
......@@ -221,5 +338,8 @@ class Grader(object):
vectors[name] = Vector(name, tail[0], tail[1], tip[0], tip[1])
return vectors
def _get_points(self, answer):
def _get_points(self, answer): # pylint: disable=no-self-use
"""
Turn point info in `answer` into a dictionary of Point objects.
"""
return {name: Point(*coords) for name, coords in answer['points'].iteritems()}
......@@ -13,9 +13,9 @@ from xblockutils.studio_editable import StudioEditableXBlockMixin
from .grader import Grader
loader = ResourceLoader(__name__)
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
log = logging.getLogger(__name__)
log = logging.getLogger(__name__) # pylint: disable=invalid-name
class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
......@@ -131,7 +131,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
display_name="Vectors",
help=(
"List of vectors to use for the exercise. "
"You must specify it as an array of entries where each entry represents an individual vector."
"You must specify it as an array of entries "
"where each entry represents an individual vector."
),
default="[]",
multiline_editor=True,
......@@ -143,7 +144,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
display_name="Points",
help=(
"List of points to be drawn on the board for reference, or to be placed by the student."
"You must specify it as an array of entries where each entry represents an individual point."
"You must specify it as an array of entries "
"where each entry represents an individual point."
),
default="[]",
multiline_editor=True,
......@@ -167,7 +169,8 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
display_name="Custom checks",
help=(
'List of custom checks to use for grading. '
'This is needed when grading is more complex and cannot be defined in terms of "Expected results" only.'
'This is needed when grading is more complex '
'and cannot be defined in terms of "Expected results" only.'
),
default="[]",
multiline_editor=True,
......@@ -216,6 +219,9 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
@property
def settings(self):
"""
Return settings for this exercise.
"""
return {
'width': self.width,
'height': self.height,
......@@ -227,13 +233,16 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
'add_vector_label': self.add_vector_label,
'vector_properties_label': self.vector_properties_label,
'background': self.background,
'vectors': self.vectors_json,
'points': self.points_json,
'expected_result': self.expected_result_json
'vectors': self.get_vectors,
'points': self.get_points,
'expected_result': self.get_expected_result
}
@property
def user_state(self):
"""
Return user state, which is a combination of most recent answer and result.
"""
user_state = self.answer
if self.result:
user_state['result'] = self.result
......@@ -241,6 +250,9 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
@property
def background(self):
"""
Return information about background to draw for this exercise.
"""
return {
'src': self.background_url,
'width': self.background_width,
......@@ -248,15 +260,25 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
}
@property
def vectors_json(self):
def get_vectors(self):
"""
Load info about vectors for this exercise from JSON string specified by course author.
"""
return json.loads(self.vectors)
@property
def points_json(self):
def get_points(self):
"""
Load info about points for this exercise from JSON string specified by course author.
"""
return json.loads(self.points)
@property
def expected_result_json(self):
def get_expected_result(self):
"""
Load info about expected result for this exercise
from JSON string specified by course author.
"""
return json.loads(self.expected_result)
def student_view(self, context=None):
......@@ -267,14 +289,20 @@ 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_url(
"//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css"
)
fragment.add_css(loader.load_unicode('static/css/vectordraw.css'))
fragment.add_javascript_url("//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js")
fragment.add_javascript_url(
"//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js"
)
fragment.add_javascript(loader.load_unicode("static/js/src/vectordraw.js"))
fragment.initialize_js('VectorDrawXBlock', {"settings": self.settings, "user_state": self.user_state})
fragment.initialize_js(
'VectorDrawXBlock', {"settings": self.settings, "user_state": self.user_state}
)
return fragment
def is_valid(self, data):
def is_valid(self, data): # pylint: disable=no-self-use
"""
Validate answer data submitted by user.
"""
......@@ -308,7 +336,7 @@ class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
return 'checks' in data
@XBlock.json_handler
def check_answer(self, data, suffix=''):
def check_answer(self, data, suffix=''): # pylint: disable=unused-argument
"""
Check and persist student's answer to this vector drawing problem.
"""
......
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