Commit ded76fc8 by Tim Krones

Merge pull request #1 from open-craft/initial-implementation

Initial implementation
parents 4fdb02a5 461371b2
__pycache__/
*.py[cod]
tests.integration.*.log
tests.integration.*.png
vectordraw_xblock.egg-info/
var/
language: python
python:
- 2.7
before_install:
- export DISPLAY=:99
- sh -e /etc/init.d/xvfb start
install:
- pip install -r test-requirements.txt
- pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/base.txt
- pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/test.txt
- pip install -r $VIRTUAL_ENV/src/xblock/requirements.txt
script:
- pep8 --max-line-length=100 vectordraw
- pylint vectordraw
- ./run_tests.py --with-coverage --cover-package=vectordraw
notifications:
email: false
addons:
firefox: 36.0
[REPORTS]
reports=no
[FORMAT]
max-line-length=100
[MESSAGES CONTROL]
disable=
I,
attribute-defined-outside-init,
maybe-no-member,
star-args,
too-few-public-methods,
too-many-ancestors,
too-many-instance-attributes,
too-many-public-methods
[VARIABLES]
dummy-variables-rgx=_$|dummy|unused
git+https://github.com/edx/XBlock.git@xblock-0.4.2#egg=XBlock
git+https://github.com/edx/xblock-utils.git@b4f9b51146c7fafa12f41d54af752b8f1516dffd#egg=xblock-utils
-e .
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Run tests for the Vector Drawing 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 logging
import sys
from django.conf import settings
from django.core.management import execute_from_command_line
logging_level_overrides = {
'workbench.views': logging.ERROR,
'django.request': logging.ERROR,
'workbench.runtime': logging.ERROR,
}
if __name__ == '__main__':
# Use the workbench settings file:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'workbench.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')
try:
os.mkdir('var')
except OSError:
# May already exist.
pass
settings.INSTALLED_APPS += ('vectordraw', )
for noisy_logger, log_level in logging_level_overrides.iteritems():
logging.getLogger(noisy_logger).setLevel(log_level)
args_iter = iter(sys.argv[1:])
options = []
paths = []
for arg in args_iter:
if arg == '--':
break
if arg.startswith('-'):
options.append(arg)
else:
paths.append(arg)
paths.extend(args_iter)
if not paths:
paths = ['tests/']
execute_from_command_line([sys.argv[0], 'test'] + options + ['--'] + paths)
"""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',
'xblock-utils',
],
entry_points={
'xblock.v1': [
'vectordraw = vectordraw:VectorDrawXBlock',
]
},
package_data=package_data("vectordraw", ["static", "public"]),
)
Django>=1.8, <1.9
-r requirements.txt
-e git+https://github.com/edx/xblock-sdk.git@8eb5f174dc59c0f4e40e10eaab56753958651d17#egg=xblock-sdk
ddt
selenium==2.47.3 # 2.48 is not working atm
import json
from ddt import ddt, data
from selenium.common.exceptions import NoSuchElementException
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable_test import StudioEditableBaseTest
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
@ddt
class TestVectorDraw(StudioEditableBaseTest):
"""
Test student view of VectorDrawXBlock.
"""
def load_scenario(self, path, params=None):
scenario = loader.render_template(path, params)
self.set_scenario_xml(scenario)
self.element = self.go_to_view("student_view")
self.exercise = self.element.find_element_by_css_selector(".vectordraw_block")
def assert_not_present(self, parent, selector, errmsg):
try:
parent.find_element_by_css_selector(selector)
except NoSuchElementException:
pass
else:
self.fail(errmsg)
def assert_hidden_text(self, selector, expected_text):
hidden_text = self.browser.execute_script("return $('{}').text();".format(selector))
self.assertEquals(hidden_text, expected_text)
def assert_title_and_description(self, expected_title="Vector Drawing", expected_description=None):
title = self.exercise.find_element_by_css_selector("h2")
self.assertEquals(title.text, expected_title)
if expected_description:
description = self.exercise.find_element_by_css_selector(".vectordraw-description")
self.assertEquals(description.text, expected_description)
else:
self.assert_not_present(
self.exercise,
".vectordraw-description",
"Description element present even though no description has been set by user."
)
def assert_dimensions(self, board, expected_width="550px", expected_height="400px"):
width = board.value_of_css_property("width")
height = board.value_of_css_property("height")
self.assertEquals(width, expected_width)
self.assertEquals(height, expected_height)
def assert_axis(self, board, is_present=False):
text_elements = board.find_elements_by_css_selector(".JXGtext")
ticks = any("ticks" in text_element.get_attribute("id") for text_element in text_elements)
self.assertEquals(ticks, is_present)
def assert_navigation_bar(self, board, is_present=False):
if is_present:
navigation_bar = board.find_element_by_css_selector("#jxgboard1_navigationbar")
self.assertTrue(navigation_bar.is_displayed())
else:
self.assert_not_present(
board,
"#jxgboard1_navigationbar",
"Navigation bar should be hidden by default."
)
def assert_background(self, board, is_present=False):
if is_present:
background = board.find_element_by_css_selector("image")
self.assertTrue(background.is_displayed())
src = background.get_attribute("xlink:href")
self.assertEquals(src, "https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png")
alt = background.get_attribute("alt")
self.assertEquals(alt, "A very informative description")
else:
self.assert_not_present(
board,
"image",
"Board should not contain background image by default."
)
def assert_buttons(self, controls, add_vector_label="Add Selected Force"):
# "Add vector" button
add_vector = controls.find_element_by_css_selector(".add-vector")
self.assertEquals(add_vector.text, add_vector_label)
# "Reset" button
reset = controls.find_element_by_css_selector(".reset")
reset_label = reset.find_element_by_css_selector('.reset-label')
self.assertEquals(reset_label.text, "Reset")
reset.find_element_by_css_selector(".sr")
self.assert_hidden_text(".reset > .sr", "Reset board to initial state")
# "Redo" button
redo = controls.find_element_by_css_selector(".redo")
redo.find_element_by_css_selector(".fa.fa-repeat")
redo.find_element_by_css_selector(".sr")
self.assert_hidden_text(".redo > .sr", "Redo last action")
# "Undo" button
undo = controls.find_element_by_css_selector(".undo")
undo.find_element_by_css_selector(".fa.fa-undo")
undo.find_element_by_css_selector(".sr")
self.assert_hidden_text(".undo > .sr", "Undo last action")
def assert_vector_properties(
self, menu, is_present=False, expected_label="Vector Properties",
expected_name=None, expected_tail=None, expected_length=None, expected_angle=None,
input_fields_disabled=True
):
if is_present:
vector_properties = menu.find_element_by_css_selector(".vector-properties")
vector_properties_label = vector_properties.find_element_by_css_selector("h3")
self.assertEquals(vector_properties_label.text, expected_label)
# Name
self.assert_vector_property(
vector_properties, "name", "select", "name:", expected_name or "-",
field_disabled=input_fields_disabled
)
# Tail
self.assert_vector_property(
vector_properties, "tail", "input", "tail position:", expected_tail or "",
field_disabled=input_fields_disabled
)
# Length
self.assert_vector_property(
vector_properties, "length", "input", "length:", expected_length or "",
field_disabled=input_fields_disabled
)
# Angle
self.assert_vector_property(
vector_properties, "angle", "input", "angle:", expected_angle or "",
field_disabled=input_fields_disabled
)
# Slope
vector_slope = vector_properties.find_element_by_css_selector(".vector-prop-slope")
self.assertFalse(vector_slope.is_displayed())
# "Update" button
update_button = vector_properties.find_element_by_css_selector('button.update')
update_button_disabled = update_button.get_attribute('disabled')
self.assertEquals(bool(update_button_disabled), input_fields_disabled)
else:
self.assert_not_present(
menu,
".vector-properties",
"If show_vector_properties is set to False, menu should not show vector properties."
)
def assert_vector_property(
self, vector_properties, property_name, input_type, expected_label, expected_value=None,
field_disabled=False
):
vector_property = vector_properties.find_element_by_css_selector(
".vector-prop-{}".format(property_name)
)
vector_property_label = vector_property.find_element_by_css_selector(
"#vector-prop-{}-label".format(property_name)
)
self.assertEquals(vector_property_label.text, expected_label)
vector_property_input = vector_property.find_element_by_css_selector(input_type)
self.assertEquals(
vector_property_input.get_attribute("aria-labelledby"), "vector-prop-{}-label".format(property_name)
)
if input_type == "input":
self.assertEquals(vector_property_input.get_attribute("value"), expected_value)
disabled = vector_property_input.get_attribute("disabled")
self.assertEquals(bool(disabled), field_disabled)
else:
selected_option = vector_property_input.find_element_by_css_selector('option[selected="selected"]')
self.assertEquals(selected_option.text, expected_value)
def assert_actions(self):
actions = self.exercise.find_element_by_css_selector(".action")
self.assertTrue(actions.is_displayed())
check = actions.find_element_by_css_selector(".check")
check_label = check.find_element_by_css_selector(".check-label")
self.assertEquals(check_label.text, "CHECK")
check.find_element_by_css_selector(".sr")
self.assert_hidden_text(".check > .sr", "Check your answer")
def assert_add_dropdown(self, controls, vectors=[], points=[]):
# Check dropdown
dropdown = controls.find_element_by_css_selector("select")
if not vectors and not points:
self.assert_not_present(
dropdown,
"option",
"Dropdown should not list any vectors or points by default."
)
else:
self.assert_add_options(dropdown, vectors, "vector")
non_fixed_points = [point for point in points if not point["fixed"]]
self.assert_add_options(dropdown, non_fixed_points, "point")
# Check label
label_selector = "label.sr"
select_label = controls.find_element_by_css_selector(label_selector)
self.assert_hidden_text(label_selector, "Select element to add to board")
select_id = "element-list"
self.assertEquals(select_label.get_attribute("for"), select_id)
def assert_add_options(self, dropdown, elements, element_type):
element_options = dropdown.find_elements_by_css_selector('option[value^="{}-"]'.format(element_type))
self.assertEquals(len(element_options), len(elements))
for element, element_option in zip(elements, element_options):
self.assertEquals(element_option.text, element["description"])
option_disabled = element_option.get_attribute("disabled")
self.assertEquals(bool(option_disabled), element["render"])
def assert_edit_dropdown(self, menu, vectors=[], points=[]):
vector_properties = menu.find_element_by_css_selector(".vector-properties")
# Check dropdown
dropdown = vector_properties.find_element_by_css_selector("select")
if not vectors and not points:
options = dropdown.find_elements_by_css_selector("option")
self.assertEquals(len(options), 1)
default_option = options[0]
self.assertEquals(default_option.get_attribute("value"), "-")
else:
if vectors:
self.assert_edit_options(dropdown, vectors, "vector")
if points:
non_fixed_points = [point for point in points if not point["fixed"]]
self.assert_edit_options(dropdown, non_fixed_points, "point")
def assert_edit_options(self, dropdown, elements, element_type):
element_options = dropdown.find_elements_by_css_selector('option[value^="{}-"]'.format(element_type))
self.assertEquals(len(element_options), len(elements))
for element, element_option in zip(elements, element_options):
self.assertEquals(element_option.text, element["name"])
option_disabled = element_option.get_attribute("disabled")
self.assertNotEquals(bool(option_disabled), element["render"])
def assert_vectors(self, board, vectors):
line_elements = board.find_elements_by_css_selector("line")
point_elements = board.find_elements_by_css_selector("ellipse")
for vector in vectors:
# Find line
board_has_line = self.board_has_line(vector["expected_line_position"], line_elements)
# Find tail
board_has_tail = self.board_has_point(vector["expected_tail_position"], point_elements)
# Find tip
board_has_tip = self.board_has_point(vector["expected_tip_position"], point_elements)
# Find label
board_has_label = self.board_has_label(board, vector["name"])
# Check if line, tip, tail are present
if vector["render"]:
self.assertTrue(board_has_line)
self.assertTrue(board_has_tail)
self.assertTrue(board_has_tip)
self.assertTrue(board_has_label)
else:
self.assertFalse(board_has_line)
self.assertFalse(board_has_tail)
self.assertFalse(board_has_tip)
self.assertFalse(board_has_label)
def assert_points(self, board, points):
point_elements = board.find_elements_by_css_selector("ellipse")
for point in points:
board_has_point = self.board_has_point(point["expected_position"], point_elements)
self.assertEquals(board_has_point, point["render"])
def board_has_line(self, position, line_elements):
line = self.find_line(position, line_elements)
return bool(line) and self.line_has_title(line) and self.line_has_desc(line)
def board_has_point(self, position, point_elements):
return bool(self.find_point(position, point_elements))
def board_has_label(self, board, label_text):
text_elements = board.find_elements_by_css_selector(".JXGtext")
for text_element in text_elements:
is_tick = "ticks" in text_element.get_attribute("id")
if not is_tick and text_element.text == label_text:
return True
return False
def line_has_title(self, line):
title = line.find_element_by_css_selector("title")
title_id = title.get_attribute("id")
aria_labelledby = line.get_attribute("aria-labelledby")
return title_id == aria_labelledby
def line_has_desc(self, line):
aria_describedby = line.get_attribute("aria-describedby")
return aria_describedby == "jxgboard1-vector-properties"
def find_line(self, position, line_elements):
expected_line_position = position.items()
for line in line_elements:
line_position = {
"x1": int(line.get_attribute("x1").split(".", 1)[0]),
"y1": int(line.get_attribute("y1").split(".", 1)[0]),
"x2": int(line.get_attribute("x2").split(".", 1)[0]),
"y2": int(line.get_attribute("y2").split(".", 1)[0]),
}.items()
if line_position == expected_line_position:
return line
def find_point(self, position, point_elements):
expected_position = position.items()
for point in point_elements:
point_position = {
"cx": int(point.get_attribute("cx").split(".", 1)[0]),
"cy": int(point.get_attribute("cy").split(".", 1)[0]),
}.items()
if point_position == expected_position:
return point
def add_vector(self, board, vectors):
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
add_vector = controls.find_element_by_css_selector(".add-vector")
add_vector.click()
# Board should now show vector
vectors[0]["render"] = True
self.assert_vectors(board, vectors)
# "Vector Properties" should display correct info
self.assert_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00",
input_fields_disabled=False
)
self.assert_edit_dropdown(menu, vectors)
def add_point(self, board, points):
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
add_vector = controls.find_element_by_css_selector(".add-vector")
add_vector.click()
# Board should now show point
points[0]["render"] = True
self.assert_points(board, points)
def undo(self, board, vectors):
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
undo = controls.find_element_by_css_selector(".undo")
undo.click()
# Board should not show vector anymore
vectors[0]["render"] = False
self.assert_vectors(board, vectors)
def redo(self, board, vectors):
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
redo = controls.find_element_by_css_selector(".redo")
redo.click()
# Board should now show vector
vectors[0]["render"] = True
self.assert_vectors(board, vectors)
# "Vector Properties" should display correct info
self.assert_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00",
input_fields_disabled=False
)
def reset(self, board, vectors, points):
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
reset = controls.find_element_by_css_selector(".reset")
reset.click()
# Board should not show vector anymore
vectors[0]["render"] = False
self.assert_vectors(board, vectors)
# Board should not show point anymore
points[0]["render"] = False
self.assert_points(board, points)
def submit_answer(self):
actions = self.exercise.find_element_by_css_selector(".action")
check = actions.find_element_by_css_selector(".check")
check.click()
def assert_status(self, answer_correct=True, expected_message="Test passed"):
status = self.exercise.find_element_by_css_selector(".vectordraw-status")
self.assertTrue(status.is_displayed())
correctness = status.find_element_by_css_selector(".correctness")
if answer_correct:
self.assertIn("checkmark-correct fa fa-check", correctness.get_attribute("class"))
else:
self.assertIn("checkmark-incorrect fa fa-times", correctness.get_attribute("class"))
status_message = status.find_element_by_css_selector(".status-message")
self.assertEquals(status_message.text, expected_message)
def change_property(self, property_name, new_value):
menu = self.exercise.find_element_by_css_selector(".menu")
vector_properties = menu.find_element_by_css_selector(".vector-properties")
vector_property = vector_properties.find_element_by_css_selector(
".vector-prop-{}".format(property_name)
)
vector_property_input = vector_property.find_element_by_css_selector("input")
# Enter new value
vector_property_input.clear()
vector_property_input.send_keys(new_value)
# Find "Update" button
update_button = vector_properties.find_element_by_css_selector(".vector-prop-update")
# Click "Update" button
update_button.click()
def test_defaults(self):
self.load_scenario("xml/defaults.xml")
# Check title and description
self.assert_title_and_description()
# Check board
board = self.exercise.find_element_by_css_selector("#jxgboard1")
self.assert_dimensions(board)
self.assert_axis(board, is_present=True)
self.assert_navigation_bar(board)
self.assert_background(board)
# - Vectors
self.assert_not_present(
board,
"line[aria-labelledby]", # axes (which are present by default) don't have aria-labelledby attribute
"Board should not contain any vectors or lines by default."
)
# - Points
self.assert_not_present(
board,
"ellipse:not([display])", # points don't have in-line "display" property
"Board should not contain any points by default."
)
# Check menu
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
self.assert_add_dropdown(controls)
self.assert_buttons(controls)
self.assert_vector_properties(menu, is_present=True)
self.assert_edit_dropdown(menu)
# Check actions
self.assert_actions()
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": True,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}, {
"name": "f",
"description": "Friction - f",
"coords": [
[-2, -5],
[-1, -3]
],
"render": False,
"expected_line_position": {"x1": 257, "y1": 340, "x2": 273, "y2": 304},
"expected_tail_position": {"cx": 257, "cy": 340},
"expected_tip_position": {"cx": 279, "cy": 294},
}
]),
"points": json.dumps([
{
"name": "cmA",
"description": "Point A",
"coords": [-0.1, -2.2],
"render": True,
"fixed": True,
"expected_position": {"cx": 300, "cy": 276},
},
{
"name": "cmB",
"description": "Point B",
"coords": [-4.0, 0.21],
"render": True,
"fixed": False,
"expected_position": {"cx": 211, "cy": 222},
},
{
"name": "cmC",
"description": "Point C",
"coords": [2.5, 2.9],
"render": False,
"fixed": False,
"expected_position": {"cx": 359, "cy": 161},
}
]),
"expected_result": json.dumps({})
},
{
"show_vector_properties": False,
"vectors": json.dumps([]),
"points": json.dumps([]),
"expected_result": json.dumps({})
},
)
def test_custom_exercise(self, params):
vectors = json.loads(params["vectors"])
points = json.loads(params["points"])
self.load_scenario("xml/custom.xml", params=params)
# Check title and description
self.assert_title_and_description(
expected_title="Custom Exercise", expected_description="Custom exercise description"
)
# Check board
board = self.exercise.find_element_by_css_selector("#jxgboard1")
self.assert_dimensions(board, expected_width="600px", expected_height="450px")
self.assert_axis(board)
self.assert_navigation_bar(board, is_present=True)
self.assert_background(board, is_present=True)
# - Vectors
self.assert_vectors(board, vectors)
# - Points
self.assert_points(board, points)
# Check menu
menu = self.exercise.find_element_by_css_selector(".menu")
controls = menu.find_element_by_css_selector(".controls")
self.assert_add_dropdown(controls, vectors, points)
self.assert_buttons(controls, add_vector_label="Custom button label")
show_vector_properties = params["show_vector_properties"]
if show_vector_properties:
self.assert_vector_properties(menu, is_present=True, expected_label="Custom properties label")
self.assert_edit_dropdown(menu, vectors, points)
else:
self.assert_vector_properties(menu)
# Check actions
self.assert_actions()
@data("line", "tail", "tip")
def test_select_vector(self, click_target):
params = {
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": True,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
vector = json.loads(params["vectors"])[0]
if click_target == "line":
# Find line and click it
line_elements = board.find_elements_by_css_selector("line")
line = self.find_line(vector["expected_line_position"], line_elements)
line.click()
elif click_target == "tail":
# Find tail and click it
point_elements = board.find_elements_by_css_selector("ellipse")
tail = self.find_point(vector["expected_tail_position"], point_elements)
tail.click()
else:
# Find tip and click it
point_elements = board.find_elements_by_css_selector("ellipse")
tip = self.find_point(vector["expected_tip_position"], point_elements)
tip.click()
# Check if "Vector Properties" shows correct info
menu = self.exercise.find_element_by_css_selector(".menu")
self.assert_vector_properties(
menu, is_present=True, expected_label="Custom properties label",
expected_name="N", expected_tail="2.00, 2.00", expected_length="4.00", expected_angle="45.00",
input_fields_disabled=False
)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_add_vector(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([]),
"points": json.dumps([
{
"name": "cmC",
"description": "Point C",
"coords": [2.5, 2.9],
"render": False,
"fixed": False,
"expected_position": {"cx": 359, "cy": 161},
}
]),
"expected_result": json.dumps({})
}
)
def test_add_point(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show point initially
points = json.loads(params["points"])
self.assert_points(board, points)
# Add point
self.add_point(board, points)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_undo(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Undo
self.undo(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_redo(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Undo
self.undo(board, vectors)
# Redo
self.redo(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([
{
"name": "cmC",
"description": "Point C",
"coords": [2.5, 2.9],
"render": False,
"fixed": False,
"expected_position": {"cx": 359, "cy": 161},
}
]),
"expected_result": json.dumps({})
}
)
def test_reset(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Board should not show point initially
points = json.loads(params["points"])
self.assert_points(board, points)
# Add vector
self.add_vector(board, vectors)
# Add point
self.add_point(board, points)
# Reset
self.reset(board, vectors, points)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({
"N": {"angle": 45, "tail": [2, 2]},
})
}
)
def test_correct_answer(self, params):
# Logic for checking answer is covered by unit tests;
# we are only checking UI behavior here.
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
vectors = json.loads(params["vectors"])
# Add vector
self.add_vector(board, vectors)
# Submit answer
self.submit_answer()
# Check status
self.assert_status()
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({
"N": {"angle": 110, "tail": [-0.6, 0.4]},
})
}
)
def test_incorrect_answer(self, params):
# Logic for checking answer is covered by unit tests;
# we are only checking UI behavior here.
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
vectors = json.loads(params["vectors"])
# Add vector
self.add_vector(board, vectors)
# Submit answer
self.submit_answer()
# Check status
self.assert_status(
answer_correct=False, expected_message="Vector N does not start at correct point."
)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({
"N": {"angle": 45, "tail": [2, 2]},
}),
"answer_correct": True
},
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({
"N": {"angle": 110, "tail": [-0.6, 0.4]},
}),
"answer_correct": False
}
)
def test_state(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
vectors = json.loads(params["vectors"])
# Board should not show vector initially
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Submit answer
self.submit_answer()
# Reload page
self.element = self.go_to_view("student_view")
self.exercise = self.element.find_element_by_css_selector(".vectordraw_block")
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should show vector
vectors[0]["render"] = True
self.assert_vectors(board, vectors)
# Status should show last result
if params["answer_correct"]:
self.assert_status()
else:
self.assert_status(
answer_correct=False, expected_message="Vector N does not start at correct point."
)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_tail_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("tail", "3, 3")
# Check new position: Tail updated, tip updated
vectors[0]["expected_line_position"] = {'x1': 370, 'y1': 159, 'x2': 425, 'y2': 102}
vectors[0]["expected_tail_position"] = {'cx': 369, 'cy': 158}
vectors[0]["expected_tip_position"] = {'cx': 434, 'cy': 94}
self.assert_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_length_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("length", "6")
# Check new position: Tail unchanged, tip updated
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 434, 'y2': 93}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 443, 'cy': 85}
self.assert_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_angle_property(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("angle", "170")
# Check new position: Tail unchanged, tip updated
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 269, 'y2': 167}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 258, 'cy': 165}
self.assert_vectors(board, vectors)
@data(
{
"show_vector_properties": True,
"vectors": json.dumps([
{
"name": "N",
"description": "Normal force - N",
"tail": [2, 2],
"length": 4,
"angle": 45,
"render": False,
"expected_line_position": {"x1": 347, "y1": 181, "x2": 402, "y2": 125},
"expected_tail_position": {"cx": 347, "cy": 181},
"expected_tip_position": {"cx": 411, "cy": 117},
}
]),
"points": json.dumps([]),
"expected_result": json.dumps({})
}
)
def test_change_property_invalid_input(self, params):
self.load_scenario("xml/custom.xml", params=params)
board = self.exercise.find_element_by_css_selector("#jxgboard1")
# Board should not show vector initially
vectors = json.loads(params["vectors"])
self.assert_vectors(board, vectors)
# Add vector
self.add_vector(board, vectors)
# Change tail
self.change_property("tail", "invalid")
# Check new position: Tail unchanged, tip unchanged
vectors[0]["expected_line_position"] = {'x1': 347, 'y1': 181, 'x2': 402, 'y2': 125}
vectors[0]["expected_tail_position"] = {'cx': 347, 'cy': 181}
vectors[0]["expected_tip_position"] = {'cx': 411, 'cy': 117}
self.assert_vectors(board, vectors)
# Check error message
error_message = self.exercise.find_element_by_css_selector(".update-error");
self.wait_until_visible(error_message)
self.assertEquals(error_message.text, "Invalid input.")
<vertical_demo>
<vectordraw url_name="vectordraw_example"
display_name="Custom Exercise"
description="Custom exercise description"
width="600"
height="450"
axis="false"
show_navigation="true"
show_vector_properties="{{ show_vector_properties }}"
show_slope_for_lines="true"
add_vector_label="Custom button label"
vector_properties_label="Custom properties label"
background_url="https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png"
background_width="20"
background_description="A very informative description"
vectors="{{ vectors }}"
points="{{ points }}"
expected_result="{{ expected_result }}"
/>
</vertical_demo>
<vertical_demo>
<vectordraw url_name="vectordraw_example" />
</vertical_demo>
import unittest
from vectordraw import grader
from vectordraw.grader import Vector
class VectorDrawTest(unittest.TestCase):
def __init__(self, *args, **kwargs):
super(VectorDrawTest, self).__init__(*args, **kwargs)
# Helpers
def check(self, expected=None, tolerance=1.0, vector='vec', errmsg=None):
check = {'vector': vector, 'expected': expected, 'tolerance': tolerance}
if errmsg:
check['errmsg'] = errmsg
return check
def vector(self, x1=0, y1=0, x2=1, y2=1, name='vec'):
return Vector(name, x1, y1, x2, y2)
def assertPasses(self, check_function, check, vectors):
try:
check_function(check, vectors)
except ValueError:
self.fail('{check_function} should not raise an error for check {check} and vectors {vectors}.'.format(
check_function=check_function.__name__, check=check, vectors=vectors
))
def assertFails(self, check_function, check, vectors, error_message):
try:
check_function(check, vectors)
except ValueError as e:
self.assertEquals(e.message, error_message)
else:
self.fail('{check_function} should raise an error for check {check} and vectors {vectors}.'.format(
check_function=check_function.__name__, check=check, vectors=vectors
))
# Test built-in checks
def test_check_presence(self):
errmsg = 'You need to use the othervec vector.'
vectors = {'myvec': self.vector(name='myvec')}
self.assertPasses(grader.check_presence, self.check(vector='myvec'), vectors)
self.assertFails(grader.check_presence, self.check(vector='othervec'), vectors, errmsg)
custom_errmsg = 'Use {name}, please!'
self.assertFails(
grader.check_presence, self.check(vector='vec X', errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec X')
)
def test_check_tail(self):
errmsg = 'Vector vec does not start at correct point.'
vectors = {'vec': self.vector(3, 3, 4, 4)}
self.assertPasses(grader.check_tail, self.check([3, 3], 0), vectors)
self.assertPasses(grader.check_tail, self.check([4, 4], 1.5), vectors)
self.assertFails(grader.check_tail, self.check([3.1, 3], 0), vectors, errmsg)
self.assertFails(grader.check_tail, self.check([4, 4], 1.0), vectors, errmsg)
custom_errmsg = 'Bad start point: [{tail_x}, {tail_y}]'
self.assertFails(
grader.check_tail, self.check([1, 2], errmsg=custom_errmsg), vectors,
custom_errmsg.format(tail_x=3, tail_y=3)
)
def test_check_tip(self):
errmsg = 'Vector vec does not end at correct point.'
vectors = {'vec': self.vector(3, 3, 4, 4)}
self.assertPasses(grader.check_tip, self.check([4, 4], 0), vectors)
self.assertPasses(grader.check_tip, self.check([3, 3], 1.5), vectors)
self.assertFails(grader.check_tip, self.check([4.1, 4], 0), vectors, errmsg)
self.assertFails(grader.check_tip, self.check([3, 3], 1.0), vectors, errmsg)
custom_errmsg = '{name} does not start at correct point.'
self.assertFails(
grader.check_tip, self.check([3, 3], errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec')
)
def test_check_tail_x(self):
errmsg = 'Vector vec does not start at correct point.'
vectors = {'vec': self.vector(3, 12, 4, 40)}
self.assertPasses(grader.check_tail_x, self.check(3, 0), vectors)
self.assertPasses(grader.check_tail_x, self.check(4, 1), vectors)
self.assertFails(grader.check_tail_x, self.check(5, 0), vectors, errmsg)
self.assertFails(grader.check_tail_x, self.check(5, 1.5), vectors, errmsg)
custom_errmsg = 'Bad starting point for {name}.'
self.assertFails(
grader.check_tail_x, self.check(5, 1.5, errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec')
)
def test_check_tail_y(self):
errmsg = 'Vector vec does not start at correct point.'
vectors = {'vec': self.vector(3, 12, 4, 40)}
self.assertPasses(grader.check_tail_y, self.check(12, 0), vectors)
self.assertPasses(grader.check_tail_y, self.check(13, 1), vectors)
self.assertFails(grader.check_tail_y, self.check(13, 0), vectors, errmsg)
self.assertFails(grader.check_tail_y, self.check(10, 1.5), vectors, errmsg)
custom_errmsg = 'Tail y should not be {tail_y:.1f}.'
self.assertFails(
grader.check_tail_y, self.check(10, 1.5, errmsg=custom_errmsg), vectors,
custom_errmsg.format(tail_y=12)
)
def test_check_tip_x(self):
errmsg = 'Vector vec does not end at correct point.'
vectors = {'vec': self.vector(3, 12, 4, 40)}
self.assertPasses(grader.check_tip_x, self.check(4, 0), vectors)
self.assertPasses(grader.check_tip_x, self.check(5, 1), vectors)
self.assertFails(grader.check_tip_x, self.check(5, 0), vectors, errmsg)
self.assertFails(grader.check_tip_x, self.check(2, 1.5), vectors, errmsg)
custom_errmsg = 'Adjust the x tip coordinate.'
self.assertFails(
grader.check_tip_x, self.check(2, 1.5, errmsg=custom_errmsg), vectors,
custom_errmsg
)
def test_check_tip_y(self):
errmsg = 'Vector vec does not end at correct point.'
vectors = {'vec': self.vector(3, 12, 4, 40)}
self.assertPasses(grader.check_tip_y, self.check(40, 0), vectors)
self.assertPasses(grader.check_tip_y, self.check(33, 10), vectors)
self.assertFails(grader.check_tip_y, self.check(41, 0), vectors, errmsg)
self.assertFails(grader.check_tip_y, self.check(29, 10), vectors, errmsg)
custom_errmsg = 'Adjust the y tip coordinate of {name}.'
self.assertFails(
grader.check_tip_y, self.check(29, 10, errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec')
)
def test_check_coords(self):
errmsg = 'Vector vec coordinates are not correct.'
vectors = {'vec': self.vector(1, 2, 3, 4)}
self.assertPasses(grader.check_coords, self.check([[1, 2], [3, 4]], 0), vectors)
self.assertPasses(grader.check_coords, self.check([[1, 3], [4, 4]], 2), vectors)
self.assertPasses(grader.check_coords, self.check([['_', 2], [3, '_']], 0), vectors)
self.assertPasses(grader.check_coords, self.check([[3, '_'], ['_', 5]], 2), vectors)
self.assertPasses(grader.check_coords, self.check([['_', '_'], ['_', 3]], 2), vectors)
self.assertPasses(grader.check_coords, self.check([['_', '_'], ['_', '_']], 0), vectors)
self.assertFails(grader.check_coords, self.check([[2, 1], [3, 4]], 0), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([[3, 4], [1, 2]], 0), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([[1, 2], [4, 3]], 1), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([[3, 4], [1, 2]], 0), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([['_', 5], [3, 4]], 1), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([['_', 2], [1, '_']], 1), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([['_', 4], [2, '_']], 1), vectors, errmsg)
self.assertFails(grader.check_coords, self.check([['_', 4], ['_', '_']], 1), vectors, errmsg)
custom_errmsg = 'Wrong coordinates: [{tail_x:.1f},{tail_y:.1f}, {tip_x:.1f},{tip_y:.1f}]'
self.assertFails(
grader.check_coords, self.check([['_', '_'], ['_', -4]], errmsg=custom_errmsg), vectors,
custom_errmsg.format(tail_x=1, tail_y=2, tip_x=3, tip_y=4)
)
def test_check_segment_coords(self):
errmsg = 'Segment vec coordinates are not correct.'
vectors = {'vec': self.vector(1, 2, 3, 4)}
self.assertPasses(grader.check_segment_coords, self.check([[1, 3], [4, 4]], 2), vectors)
self.assertPasses(grader.check_segment_coords, self.check([[4, 4], [1, 3]], 2), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', 2], [3, '_']], 0), vectors)
self.assertPasses(grader.check_segment_coords, self.check([[3, '_'], ['_', 2]], 0), vectors)
self.assertPasses(grader.check_segment_coords, self.check([[3, '_'], ['_', 5]], 2), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', 5], [3, '_']], 2), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', '_'], ['_', 3]], 2), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', '_'], ['_', '_']], 0), vectors)
self.assertPasses(grader.check_segment_coords, self.check([[1, 2], [3, 4]], 0), vectors)
self.assertPasses(grader.check_segment_coords, self.check([[3, 4], [1, 2]], 0), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', 4], ['_', '_']], 1), vectors)
self.assertPasses(grader.check_segment_coords, self.check([['_', 4], [2, '_']], 1), vectors)
self.assertFails(grader.check_segment_coords, self.check([[2, 1], [3, 4]], 0), vectors, errmsg)
self.assertFails(grader.check_segment_coords, self.check([[-1, -2], [-3, -4]], 0), vectors, errmsg)
self.assertFails(grader.check_segment_coords, self.check([[1, 2], [4, 3]], 1), vectors, errmsg)
self.assertFails(grader.check_segment_coords, self.check([['_', 5], [3, 4]], 1), vectors, errmsg)
self.assertFails(grader.check_segment_coords, self.check([['_', 2], [1, '_']], 1), vectors, errmsg)
self.assertFails(grader.check_segment_coords, self.check([['_', '_'], ['_', 5.5]], 1), vectors, errmsg)
custom_errmsg = 'Coordinates of {name} are wrong!'
self.assertFails(
grader.check_segment_coords, self.check([['_', '_'], ['_', 5.5]], errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec')
)
def test_check_length(self):
errmsg = 'The length of vec is incorrect. Your length: 5.0'
vectors = {'vec': self.vector(0, 0, 3, 4)}
self.assertPasses(grader.check_length, self.check(5, 0), vectors)
self.assertPasses(grader.check_length, self.check(7, 2.5), vectors)
self.assertFails(grader.check_length, self.check(4.5, 0), vectors, errmsg)
self.assertFails(grader.check_length, self.check(5.5, 0.25), vectors, errmsg)
custom_errmsg = 'Bad length of {length:.2f}'
self.assertFails(
grader.check_length, self.check(5.5, 0.25, errmsg=custom_errmsg), vectors,
custom_errmsg.format(length=5.00)
)
def test_check_angle(self):
errmsg = 'The angle of vec is incorrect. Your angle: 45.0'
vectors = {'vec': self.vector(1, 1, 5, 5)}
self.assertPasses(grader.check_angle, self.check(45, 0.1), vectors)
self.assertPasses(grader.check_angle, self.check(-315, 0.1), vectors)
self.assertPasses(grader.check_angle, self.check(405, 0.1), vectors)
self.assertPasses(grader.check_angle, self.check(-5, 55), vectors)
self.assertPasses(grader.check_angle, self.check(42, 5), vectors)
self.assertFails(grader.check_angle, self.check(315, 0.1), vectors, errmsg)
self.assertFails(grader.check_angle, self.check(30, 9), vectors, errmsg)
custom_errmsg = 'Adjust angle of {name}. Currently: {angle:.0f}.'
self.assertFails(
grader.check_angle, self.check(30, 9, errmsg=custom_errmsg), vectors,
custom_errmsg.format(name='vec', angle=45.0)
)
def test_check_segment_angle(self):
errmsg = 'The angle of vec is incorrect. Your angle: 45.0'
vectors = {'vec': self.vector(1, 1, 5, 5)}
self.assertPasses(grader.check_segment_angle, self.check(45, 0.1), vectors)
self.assertPasses(grader.check_segment_angle, self.check(-315, 0.1), vectors)
self.assertPasses(grader.check_segment_angle, self.check(405, 0.1), vectors)
self.assertPasses(grader.check_segment_angle, self.check(42, 5), vectors)
self.assertFails(grader.check_segment_angle, self.check(-405, 0.1), vectors, errmsg)
self.assertFails(grader.check_segment_angle, self.check(315, 0.1), vectors, errmsg)
self.assertFails(grader.check_segment_angle, self.check(-45, 9), vectors, errmsg)
custom_errmsg = 'Segment angle is incorrect.'
self.assertFails(
grader.check_segment_angle, self.check(-45, 9, errmsg=custom_errmsg), vectors,
custom_errmsg
)
def test_check_points_on_line(self):
errmsg = 'The line vec does not pass through the correct points.'
vectors = {'vec': self.vector(1, 1, 5, 5)}
self.assertPasses(grader.check_points_on_line, self.check([[1, 1], [5, 5]]), vectors)
self.assertPasses(grader.check_points_on_line, self.check([[1, 2], [5, 4]]), vectors)
self.assertPasses(grader.check_points_on_line, self.check([[3.7, 2.3], [2.3, 3.7]]), vectors)
self.assertPasses(grader.check_points_on_line, self.check([[-1, -.5], [98, 99]]), vectors)
self.assertFails(grader.check_points_on_line, self.check([[1, -1]]), vectors, errmsg)
self.assertFails(grader.check_points_on_line, self.check([[3.8, 2.2]]), vectors, errmsg)
self.assertFails(grader.check_points_on_line, self.check([[18, 13]]), vectors, errmsg)
vectors = {'vec': self.vector(1, 1, 1, 5)} # vertical line
self.assertPasses(grader.check_points_on_line, self.check([[1, 3], [1, 99], [1.9, 55]]), vectors)
self.assertFails(grader.check_points_on_line, self.check([[2.1, 3]]), vectors, errmsg)
vectors = {'vec': self.vector(1, 1, 5, 1)} # horizontal line
self.assertPasses(grader.check_points_on_line, self.check([[3, 1], [99, 1], [55, 1.9]]), vectors)
self.assertFails(grader.check_points_on_line, self.check([[3, 2.1]]), vectors, errmsg)
"""
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.
It uses the following data structures:
- `vectors`: A dictionary of Vector objects.
Keys are vector names and values represent individual vectors that were present
on the drawing board when the student submitted an answer by clicking the 'Check' button.
- `points`: A dictionary of Point objects.
Keys are point names and values represent individual points that were present
on the drawing board when the student submitted an answer by clicking the 'Check' button.
- `check`: A dictionary representing a specific check.
Contains the name of the check itself (e.g., 'presence', 'coords', 'angle'),
the name of the element on which to perform the check, as well as
the expected value of the property being checked.
Optionally contains information about tolerance to apply when performing the check,
and/or a custom error message to present to the user if the check fails.
- `answer`: A dictionary representing a specific answer submitted by a student.
Contains three entries: vectors, points, and checks. The first two (vectors, points)
provide information about vectors and points present on the drawing board
when the answer was submitted. The third one (checks) specifies the checks
to perform for individual vectors and points.
"""
# pylint: disable=invalid-name
import inspect
import logging
import math
log = logging.getLogger(__name__) # pylint: disable=invalid-name
# 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
)
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.')
raise ValueError(errmsg.format(name=check['vector']))
def _check_vector_endpoint(check, vectors, endpoint):
"""
Check if `endpoint` (tail or tip) of vector targeted by `check` is in correct position.
"""
vec = vectors[check['vector']]
tolerance = check.get('tolerance', 1.0)
expected = check['expected']
verb = 'start' if endpoint == 'tail' else 'end'
endpoint = getattr(vec, endpoint)
dist = math.hypot(expected[0] - endpoint.x, expected[1] - endpoint.y)
if dist > tolerance:
raise ValueError(_errmsg(
'Vector {name} does not {verb} at correct point.'.format(name='{name}', verb=verb),
check,
vectors
))
def check_tail(check, vectors):
"""
Check if tail of vector targeted by `check` is in correct position.
"""
return _check_vector_endpoint(check, vectors, endpoint='tail')
def check_tip(check, vectors):
"""
Check if tip of vector targeted by `check` is in correct position.
"""
return _check_vector_endpoint(check, vectors, endpoint='tip')
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):
raise ValueError(_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):
raise ValueError(_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):
raise ValueError(_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):
raise ValueError(_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)
if math.hypot(delta_x, delta_y) > 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):
raise ValueError(_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)
if not (_coords_within_tolerance(vec, expected, tolerance) or
_coords_within_tolerance(vec.opposite(), expected, tolerance)):
raise ValueError(_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:
raise ValueError(_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))
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):
"""
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):
raise ValueError(
_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']]
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)):
raise ValueError(
_errmsg('The angle of {name} is incorrect. Your angle: {angle:.1f}', check, vectors)
)
def _dist_line_point(line, point):
"""
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['expected']
for point in points:
point = Point(point[0], point[1])
if _dist_line_point(line, point) > tolerance:
raise ValueError(_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['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):
""" Represents a single point on the vector drawing board. """
def __init__(self, x, y):
self.x = x
self.y = y
class Vector(object):
""" 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)
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 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,
'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 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),
)
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]
try:
check_fn(*args)
except ValueError as e:
return {'correct': False, 'msg': e.message}
return {'correct': True, 'msg': self.success_message}
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']
tip = props['tip']
vectors[name] = Vector(name, tail[0], tail[1], tip[0], tip[1])
return vectors
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()}
/* 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 */
}
/* Menu */
.vectordraw_block .menu {
width: 100%;
}
.vectordraw_block .menu,
.vectordraw_block .menu .controls {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
background-color: #e0e0e0;
font-size: 0;
}
.vectordraw_block .menu {
border-top: 2px solid #1f628d;
border-left: 2px solid #1f628d;
border-right: 2px solid #1f628d;
}
.vectordraw_block .menu .controls {
border-bottom: 2px solid #1f628d;
padding: 3px;
}
.vectordraw_block .menu .controls select {
width: 160px;
margin-right: 3px;
font-size: 18px;
}
.vectordraw_block .menu button {
border: 1px solid #1f628d;
border-radius: 5px;
margin: 4px 0;
padding: 5px 10px 5px 10px;
box-shadow: 0 1px 3px #666;
background-color: #c2e0f4;
color: #1f628d;
font-size: 14px;
text-decoration: none;
}
.vectordraw_block .menu button:focus,
.vectordraw_block .menu button:hover {
background: #c2e0f4;
background-image: -webkit-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -moz-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -ms-linear-gradient(top, #c2e0f4, #add5f0);
background-image: -o-linear-gradient(top, #c2e0f4, #add5f0);
background-image: linear-gradient(to bottom, #c2e0f4, #add5f0);
text-decoration: none;
}
.vectordraw_block .menu .controls button.reset,
.vectordraw_block .menu .controls button.undo,
.vectordraw_block .menu .controls button.redo {
float: right;
}
.vectordraw_block .menu .vector-properties {
padding: 10px;
font-size: 16px;
line-height: 1.25;
background-color: #f7f7f7;
}
.vectordraw_block h3 {
font-size: 16px;
margin: 0 0 5px;
}
.vectordraw_block .menu .vector-properties .vector-prop-list {
display: table;
width: 100%
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row {
display: table-row;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop {
display: table-cell;
width: 50%
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle {
padding-right: 5px;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row span,
.vectordraw_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_block .menu .vector-properties .vector-prop-list .row input {
width: 50%;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_block .menu .vector-properties .vector-prop-list .row input {
float: right;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-update .update-error {
display: none;
color: #ff0000;
}
.vectordraw_block .menu .vector-properties button:disabled {
pointer-events: none;
border: 1px solid #707070;
background-color: #ececec;
color: #868686;
}
.vectordraw_block .action button {
height: 40px;
margin-right: 10px;
font-weight: 600;
text-transform: uppercase;
}
/* Make sure screen-reader content is hidden in the workbench: */
.vectordraw_block .sr {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
background: #ffffff;
color: #000000;
}
.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;
}
.vectordraw_edit_block,
.vectordraw_edit_block .jxgboard {
display: block;
}
.vectordraw_edit_block {
border-top: 1px solid #e5e5e5;
margin-left: 20px;
margin-right: 20px;
padding-top: 20px;
}
.vectordraw_edit_block h2 {
margin-bottom: 1em;
}
.vectordraw_edit_block #wysiwyg-description {
display: none;
}
.vectordraw_edit_block p {
margin-bottom: 1em;
font-size: 0.9em;
}
.vectordraw_edit_block .jxgboard {
float: left;
margin-bottom: 1em;
}
/* Menu */
.vectordraw_edit_block .menu .controls .result-mode {
float: right;
}
.vectordraw_edit_block .menu .controls button:disabled {
pointer-events: none;
border: 1px solid #707070;
background-color: #ececec;
color: #868686;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-name,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-tail,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-angle {
padding-right: 0px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-select,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-label,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-prop-length {
padding-right: 5px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row select,
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row input {
height: 2em;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row select {
padding: 0px;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove {
vertical-align: bottom;
}
.vectordraw_edit_block .menu .vector-properties .vector-prop-list .row .vector-remove button {
float: right;
}
.vectordraw_block .menu .vector-properties .vector-prop-list .row .vector-prop-update .update-pending {
display: none;
color: #ffa500;
}
.vectordraw_edit_block h3 {
margin-top: 5px;
margin-bottom: 5px;
}
.vectordraw_edit_block .checks {
display: none;
width: 220px;
float: right;
border-top: 2px solid #1f628d;
border-right: 2px solid #1f628d;
border-bottom: 2px solid #1f628d;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding-left: 10px;
background-color: #f7f7f7;
overflow: auto;
}
.vectordraw_edit_block .checks .check .row {
height: 2em;
}
.vectordraw_edit_block .checks .check span,
.vectordraw_edit_block .checks .check input[type="number"] {
height: 1.5em;
min-width: 0px;
margin-right: 12px;
}
.vectordraw_edit_block .checks .check input[type="number"] {
padding: 0 0 0 5px;
}
.vectordraw_edit_block .checks .check input[type="checkbox"] {
height: 1.2em;
width: 20%;
min-width: 0px;
margin-top: 2px;
}
.vectordraw_edit_block .checks .check input {
float: right;
vertical-align: middle;
}
/* Javascript for StudioEditableXBlockMixin. */
function StudioEditableXBlockMixin(runtime, element) {
"use strict";
var fields = [];
var tinyMceAvailable = (typeof $.fn.tinymce !== 'undefined'); // Studio includes a copy of tinyMCE and its jQuery plugin
var errorMessage = gettext("This may be happening because of an error with our server or your internet connection. Make sure you are online, and try refreshing the page.");
$(element).find('.field-data-control').each(function() {
var $field = $(this);
var $wrapper = $field.closest('li');
var $resetButton = $wrapper.find('button.setting-clear');
var type = $wrapper.data('cast');
fields.push({
name: $wrapper.data('field-name'),
isSet: function() { return $wrapper.hasClass('is-set'); },
hasEditor: function() { return tinyMceAvailable && $field.tinymce(); },
val: function() {
var val = $field.val();
// Cast values to the appropriate type so that we send nice clean JSON over the wire:
if (type === 'boolean')
return (val === 'true' || val === '1');
if (type === "integer")
return parseInt(val, 10);
if (type === "float")
return parseFloat(val);
return val;
},
removeEditor: function() {
$field.tinymce().remove();
}
});
var fieldChanged = function() {
// Field value has been modified:
$wrapper.addClass('is-set');
$resetButton.removeClass('inactive').addClass('active');
};
$field.bind("change input paste", fieldChanged);
$resetButton.click(function() {
$field.val($wrapper.attr('data-default')); // Use attr instead of data to force treating the default value as a string
$wrapper.removeClass('is-set');
$resetButton.removeClass('active').addClass('inactive');
});
if (type === 'html' && tinyMceAvailable) {
tinyMCE.baseURL = baseUrl + "/js/vendor/tinymce/js/tinymce";
$field.tinymce({
theme: 'modern',
skin: 'studio-tmce4',
height: '200px',
formats: { code: { inline: 'code' } },
codemirror: { path: "" + baseUrl + "/js/vendor" },
convert_urls: false,
plugins: "link codemirror",
menubar: false,
statusbar: false,
toolbar_items_size: 'small',
toolbar: "formatselect | styleselect | bold italic underline forecolor wrapAsCode | bullist numlist outdent indent blockquote | link unlink | code",
resize: "both",
setup : function(ed) {
ed.on('change', fieldChanged);
}
});
}
});
var studio_submit = function(data) {
var handlerUrl = runtime.handlerUrl(element, 'submit_studio_edits');
runtime.notify('save', {state: 'start', message: gettext("Saving")});
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
dataType: "json",
notifyOnError: false
}).done(function(response) {
runtime.notify('save', {state: 'end'});
}).fail(function(jqXHR) {
if (jqXHR.responseText) { // Is there a more specific error message we can show?
try {
errorMessage = JSON.parse(jqXHR.responseText).error;
if (_.isObject(errorMessage) && errorMessage.messages) {
// e.g. {"error": {"messages": [{"text": "Unknown user 'bob'!", "type": "error"}, ...]}} etc.
var errorMessages = _.pluck(errorMessage.messages, "text");
errorMessage = errorMessages.join(", ");
}
} catch (error) { errorMessage = jqXHR.responseText.substr(0, 300); }
}
runtime.notify('error', {title: gettext("Unable to update settings"), message: errorMessage});
});
};
return {
getContents: function(fieldName) {
return _.findWhere(fields, {name: fieldName}).val();
},
save: function(data) {
var values = {};
var notSet = []; // List of field names that should be set to default values
_.each(fields, function(field) {
if (field.isSet()) {
values[field.name] = field.val();
} else {
notSet.push(field.name);
}
// Remove TinyMCE instances to make sure jQuery does not try to access stale instances
// when loading editor for another block:
if (field.hasEditor()) {
field.removeEditor();
}
});
// If WYSIWYG editor was used,
// prefer its data over values of "Vectors" and "Expected result" fields:
if (!_.isEmpty(data)) {
values.vectors = JSON.stringify(data.vectors, undefined, 4);
values.expected_result_positions = data.expected_result_positions;
values.expected_result = JSON.stringify(data.expected_result, undefined, 4);
}
studio_submit({values: values, defaults: notSet});
},
cancel: function() {
// Remove TinyMCE instances to make sure jQuery does not try to access stale instances
// when loading editor for another block:
_.each(fields, function(field) {
if (field.hasEditor()) {
field.removeEditor();
}
});
runtime.notify('cancel', {});
}
};
}
/* Javascript for VectorDrawXBlock. */
function VectorDrawXBlock(runtime, element, init_args) {
'use strict';
// Logic for rendering and interacting with vector drawing exercise
var VectorDraw = function(element_id, settings) {
this.board = null;
this.dragged_vector = null;
this.drawMode = false;
this.history_stack = {undo: [], redo: []};
this.settings = settings;
this.element = $('#' + element_id, element);
this.element.on('click', '.reset', this.reset.bind(this));
this.element.on('click', '.add-vector', this.addElementFromList.bind(this));
this.element.on('click', '.undo', this.undo.bind(this));
this.element.on('click', '.redo', this.redo.bind(this));
this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this));
this.element.on('click', '.menu .vector-prop-update', this.onEditSubmit.bind(this));
// Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
this.render();
};
VectorDraw.prototype.render = function() {
$('.vector-prop-slope', this.element).hide();
// Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
this.createBoard();
};
VectorDraw.prototype.createBoard = function() {
var id = this.element.find('.jxgboard').prop('id'),
self = this;
this.board = JXG.JSXGraph.initBoard(id, {
keepaspectratio: true,
boundingbox: this.settings.bounding_box,
axis: this.settings.axis,
showCopyright: false,
showNavigation: this.settings.show_navigation
});
function getImageRatio(bg, callback) {
$('<img/>').attr('src', bg.src).load(function() {
//technically it's inverse of ratio, but we need it to calculate height
var ratio = this.height / this.width;
callback(bg, ratio);
});
}
function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2];
var image = self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
$(image.rendNode).attr('alt', bg.description);
}
if (this.settings.background) {
if (this.settings.background.height) {
drawBackground(this.settings.background);
}
else {
getImageRatio(this.settings.background, drawBackground);
}
}
function renderAndSetMenuOptions(element, idx, type, board) {
if (element.render) {
if (type === 'point') {
board.renderPoint(idx);
} else {
board.renderVector(idx);
}
} else {
// Enable corresponding option in menu for adding vectors ...
var addOption = board.getAddMenuOption(type, idx);
addOption.prop('disabled', false);
// ... and select it if no option is currently selected
if ($('.menu .element-list-add option').filter(':selected').length === 0) {
addOption.prop('selected', true);
}
// Disable corresponding option in menu for editing vectors
var editOption = board.getEditMenuOption(type, idx);
editOption.prop('disabled', true);
}
}
// a11y
// Generate and set unique ID for "Vector Properties";
// this is necessary to ensure that "aria-describedby" associations
// between vectors and the "Vector Properties" box don't break
// when multiple boards are present:
var vectorProperties = $(".vector-properties", element);
vectorProperties.attr("id", id + "-vector-properties");
// Draw vectors and points
_.each(this.settings.points, function(point, idx) {
renderAndSetMenuOptions(point, idx, 'point', this);
}, this);
_.each(this.settings.vectors, function(vec, idx) {
renderAndSetMenuOptions(vec, idx, 'vector', this);
}, this);
// Set up event handlers
this.board.on('down', this.onBoardDown.bind(this));
this.board.on('move', this.onBoardMove.bind(this));
this.board.on('up', this.onBoardUp.bind(this));
};
VectorDraw.prototype.renderPoint = function(idx, coords) {
var point = this.settings.points[idx];
var coords = coords || point.coords;
var board_object = this.board.elementsByName[point.name];
if (board_object) {
// If the point is already rendered, only update its coordinates.
board_object.setPosition(JXG.COORDS_BY_USER, coords);
return;
}
this.board.create('point', coords, point.style);
if (!point.fixed) {
// Disable the <option> element corresponding to point.
var option = this.getAddMenuOption('point', idx);
option.prop('disabled', true).prop('selected', false);
}
};
VectorDraw.prototype.removePoint = function(idx) {
var point = this.settings.points[idx];
var object = this.board.elementsByName[point.name];
if (object) {
this.board.removeAncestors(object);
// Enable the <option> element corresponding to point.
var option = this.getAddMenuOption('point', idx);
option.prop('disabled', false);
}
};
VectorDraw.prototype.getVectorCoordinates = function(vec) {
var coords = vec.coords;
if (!coords) {
var tail = vec.tail || [0, 0];
var length = 'length' in vec ? vec.length : 5;
var angle = 'angle' in vec ? vec.angle : 30;
var radians = angle * Math.PI / 180;
var tip = [
tail[0] + Math.cos(radians) * length,
tail[1] + Math.sin(radians) * length
];
coords = [tail, tip];
}
return coords;
};
VectorDraw.prototype.renderVector = function(idx, coords) {
var vec = this.settings.vectors[idx];
coords = coords || this.getVectorCoordinates(vec);
// If this vector is already rendered, only update its coordinates.
var board_object = this.board.elementsByName[vec.name];
if (board_object) {
board_object.point1.setPosition(JXG.COORDS_BY_USER, coords[0]);
board_object.point2.setPosition(JXG.COORDS_BY_USER, coords[1]);
return;
}
var style = vec.style;
var tail = this.board.create('point', coords[0], {
name: vec.name,
size: style.pointSize,
fillColor: style.pointColor,
strokeColor: style.pointColor,
withLabel: false,
fixed: (vec.type === 'arrow' | vec.type === 'vector'),
showInfoBox: false
});
var tip = this.board.create('point', coords[1], {
name: style.label || vec.name,
size: style.pointSize,
fillColor: style.pointColor,
strokeColor: style.pointColor,
withLabel: true,
showInfoBox: false
});
// Not sure why, but including labelColor in attributes above doesn't work,
// it only works when set explicitly with setAttribute.
tip.setAttribute({labelColor: style.labelColor});
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
var line_type = (vec.type === 'vector') ? 'arrow' : vec.type;
var line = this.board.create(line_type, [tail, tip], {
name: vec.name,
strokeWidth: style.width,
strokeColor: style.color
});
// Disable the <option> element corresponding to vector.
var option = this.getAddMenuOption('vector', idx);
option.prop('disabled', true).prop('selected', false);
// a11y
var lineElement = $(line.rendNode);
var lineID = lineElement.attr("id");
var titleID = lineID + "-title";
var titleElement = $("<title>", {"id": titleID, "text": vec.name});
lineElement.append(titleElement);
lineElement.attr("aria-labelledby", titleID);
var vectorProperties = $(".vector-properties", element);
lineElement.attr("aria-describedby", vectorProperties.attr("id"));
return line;
};
VectorDraw.prototype.removeVector = function(idx) {
var vec = this.settings.vectors[idx];
var object = this.board.elementsByName[vec.name];
if (object) {
this.board.removeAncestors(object);
// Enable the <option> element corresponding to vector.
var option = this.getAddMenuOption('vector', idx);
option.prop('disabled', false);
}
};
VectorDraw.prototype.getAddMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-add option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.getEditMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-edit option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.getSelectedElement = function() {
var selector = this.element.find('.menu .element-list-add').val();
if (selector) {
selector = selector.split('-');
return {
type: selector[0],
idx: parseInt(selector[1], 10)
};
}
return {};
};
VectorDraw.prototype.enableEditOption = function(selectedElement) {
var editOption = this.getEditMenuOption(selectedElement.type, selectedElement.idx);
editOption.prop('disabled', false);
};
VectorDraw.prototype.addElementFromList = function() {
this.pushHistory();
var selected = this.getSelectedElement();
if (selected.type === 'vector') {
this.updateVectorProperties(this.renderVector(selected.idx));
} else {
this.renderPoint(selected.idx);
}
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
};
VectorDraw.prototype.reset = function() {
this.pushHistory();
JXG.JSXGraph.freeBoard(this.board);
this.resetVectorProperties();
this.render();
};
VectorDraw.prototype.pushHistory = function() {
var state = this.getState();
var previous_state = _.last(this.history_stack.undo);
if (!_.isEqual(state, previous_state)) {
this.history_stack.undo.push(state);
this.history_stack.redo = [];
}
};
VectorDraw.prototype.undo = function() {
var curr_state = this.getState();
var undo_state = this.history_stack.undo.pop();
if (undo_state && !_.isEqual(undo_state, curr_state)) {
this.history_stack.redo.push(curr_state);
this.setState(undo_state);
}
};
VectorDraw.prototype.redo = function() {
var state = this.history_stack.redo.pop();
if (state) {
this.history_stack.undo.push(this.getState());
this.setState(state);
}
};
VectorDraw.prototype.getMouseCoords = function(evt) {
var i = evt[JXG.touchProperty] ? 0 : undefined;
var c_pos = this.board.getCoordsTopLeftCorner(evt, i);
var abs_pos = JXG.getPosition(evt, i);
var dx = abs_pos[0] - c_pos[0];
var dy = abs_pos[1] - c_pos[1];
return new JXG.Coords(JXG.COORDS_BY_SCREEN, [dx, dy], this.board);
};
VectorDraw.prototype.getVectorForObject = function(obj) {
if (obj instanceof JXG.Line) {
return obj;
}
if (obj instanceof JXG.Text) {
return this.getVectorForObject(obj.element);
}
if (obj instanceof JXG.Point) {
return _.find(obj.descendants, function (d) { return (d instanceof JXG.Line); });
}
return null;
};
VectorDraw.prototype.getVectorSettingsByName = function(name) {
return _.find(this.settings.vectors, function(vec) {
return vec.name === name;
});
};
VectorDraw.prototype.updateVectorProperties = function(vector) {
var vec_settings = this.getVectorSettingsByName(vector.name);
var x1 = vector.point1.X(),
y1 = vector.point1.Y(),
x2 = vector.point2.X(),
y2 = vector.point2.Y();
var length = vec_settings.length_factor * Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
var angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180) - vec_settings.base_angle) % 360;
if (angle < 0) {
angle += 360;
}
var slope = (y2-y1)/(x2-x1);
// Update menu for selecting vector to edit
this.element.find('.menu .element-list-edit option').attr('selected', false);
var idx = _.indexOf(this.settings.vectors, vec_settings),
editOption = this.getEditMenuOption("vector", idx);
editOption.attr('selected', true);
// Update properties
$('.vector-prop-angle input', this.element).val(angle.toFixed(2));
if (vector.elType !== "line") {
var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2);
var lengthInput = length.toFixed(2);
if (vec_settings.length_units) {
lengthInput += ' ' + vec_settings.length_units;
}
$('.vector-prop-tail input', this.element).val(tailInput);
$('.vector-prop-length', this.element).show();
$('.vector-prop-length input', this.element).val(lengthInput);
$('.vector-prop-slope', this.element).hide();
}
else {
$('.vector-prop-length', this.element).hide();
if (this.settings.show_slope_for_lines) {
$('.vector-prop-slope', this.element).show();
$('.vector-prop-slope input', this.element).val(slope.toFixed(2));
}
}
// Enable input fields
$('.vector-properties input').prop('disabled', false);
// Enable buttons
$('.vector-properties button').prop('disabled', false);
// Hide error message
$('.vector-prop-update .update-error', element).hide();
};
VectorDraw.prototype.resetVectorProperties = function(vector) {
// Reset dropdown for selecting vector to default value
$('.menu .element-list-edit option[value="-"]', element).attr('selected', true);
// Reset input fields to default values and disable them
$('.menu .vector-prop-list input', element).prop('disabled', true).val('');
// Disable "Update" button
$('.vector-properties button').prop('disabled', true);
};
VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow';
};
VectorDraw.prototype.canCreateVectorOnTopOf = function(el) {
// If the user is trying to drag the arrow of an existing vector, we should not create a new vector.
if (el instanceof JXG.Line) {
return false;
}
// If this is tip/tail of a vector, it's going to have a descendant Line - we should not create a new vector
// when over the tip. Creating on top of the tail is allowed for plain vectors but not for segments.
// If it doesn't have a descendant Line, it's a point from settings.points - creating a new vector is allowed.
if (el instanceof JXG.Point) {
var vector = this.getVectorForObject(el);
if (!vector) {
return el.getProperty('fixed');
} else if (el === vector.point1 && !this.isVectorTailDraggable(vector)) {
return true;
} else {
return false;
}
}
return true;
};
VectorDraw.prototype.objectsUnderMouse = function(coords) {
var filter = function(el) {
return !(el instanceof JXG.Image) && el.hasPoint(coords.scrCoords[1], coords.scrCoords[2]);
};
return _.filter(_.values(this.board.objects), filter);
};
VectorDraw.prototype.onBoardDown = function(evt) {
this.pushHistory();
// Can't create a vector if none is selected from the list.
var selected = this.getSelectedElement();
var coords = this.getMouseCoords(evt);
var targetObjects = this.objectsUnderMouse(coords);
if (selected.idx && (!targetObjects || _.all(targetObjects, this.canCreateVectorOnTopOf.bind(this)))) {
var point_coords = [coords.usrCoords[1], coords.usrCoords[2]];
if (selected.type === 'vector') {
this.drawMode = true;
this.dragged_vector = this.renderVector(selected.idx, [point_coords, point_coords]);
} else {
this.renderPoint(selected.idx, point_coords);
}
// Enable option corresponding to selected element in menu for selecting element to edit
this.enableEditOption(selected);
}
else {
this.drawMode = false;
var vectorPoint = _.find(targetObjects, this.getVectorForObject.bind(this));
if (vectorPoint) {
this.dragged_vector = this.getVectorForObject(vectorPoint);
this.dragged_vector.point1.setProperty({fixed: false});
this.updateVectorProperties(this.dragged_vector);
}
}
};
VectorDraw.prototype.onBoardMove = function(evt) {
if (this.drawMode) {
var coords = this.getMouseCoords(evt);
this.dragged_vector.point2.moveTo(coords.usrCoords);
}
if (this.dragged_vector) {
this.updateVectorProperties(this.dragged_vector);
}
};
VectorDraw.prototype.onBoardUp = function(evt) {
this.drawMode = false;
if (this.dragged_vector && !this.isVectorTailDraggable(this.dragged_vector)) {
this.dragged_vector.point1.setProperty({fixed: true});
}
this.dragged_vector = null;
};
VectorDraw.prototype.onEditStart = function(evt) {
var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name');
var vectorObject = this.board.elementsByName[vectorName];
this.updateVectorProperties(vectorObject);
};
VectorDraw.prototype.onEditSubmit = function(evt) {
// Get vector that is currently "selected"
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name');
// Get values from input fields
var newTail = $('.vector-prop-tail input', element).val(),
newLength = $('.vector-prop-length input', element).val(),
newAngle = $('.vector-prop-angle input', element).val();
// Process values
newTail = _.map(newTail.split(/ *, */), function(coord) {
return parseFloat(coord);
});
newLength = parseFloat(newLength);
newAngle = parseFloat(newAngle);
var values = [newTail[0], newTail[1], newLength, newAngle];
// Validate values
if (!_.some(values, Number.isNaN)) {
$('.vector-prop-update .update-error', element).hide();
// Use coordinates of new tail, new length, new angle to calculate new position of tip
var radians = newAngle * Math.PI / 180;
var newTip = [
newTail[0] + Math.cos(radians) * newLength,
newTail[1] + Math.sin(radians) * newLength
];
// Update position of vector
var board_object = this.board.elementsByName[vectorName];
board_object.point1.setPosition(JXG.COORDS_BY_USER, newTail);
board_object.point2.setPosition(JXG.COORDS_BY_USER, newTip);
this.board.update();
} else {
$('.vector-prop-update .update-error', element).show();
}
};
VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name];
if (object) {
return {
tail: [object.point1.X(), object.point1.Y()],
tip: [object.point2.X(), object.point2.Y()]
};
}
};
VectorDraw.prototype.getState = function() {
var vectors = {}, points = {};
_.each(this.settings.vectors, function(vec) {
var coords = this.getVectorCoords(vec.name);
if (coords) {
vectors[vec.name] = coords;
}
}, this);
_.each(this.settings.points, function(point) {
var obj = this.board.elementsByName[point.name];
if (obj) {
points[point.name] = [obj.X(), obj.Y()];
}
}, this);
return {vectors: vectors, points: points};
};
VectorDraw.prototype.setState = function(state) {
_.each(this.settings.vectors, function(vec, idx) {
var vec_state = state.vectors[vec.name];
if (vec_state) {
this.renderVector(idx, [vec_state.tail, vec_state.tip]);
} else {
this.removeVector(idx);
}
}, this);
_.each(this.settings.points, function(point, idx) {
var point_state = state.points[point.name];
if (point_state) {
this.renderPoint(idx, point_state);
} else {
this.removePoint(idx);
}
}, this);
this.board.update();
};
// Logic for checking answers
var checkHandlerUrl = runtime.handlerUrl(element, 'check_answer');
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);
var properties = [
'tail', 'tail_x', 'tail_y', 'tip', 'tip_x', 'tip_y', 'coords',
'length', 'angle', 'segment_angle', 'segment_coords', 'points_on_line'
];
_.each(properties, 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;
return input;
}
function updateStatus(data) {
var correctness = $('.correctness', element),
correctClass = 'checkmark-correct fa fa-check',
incorrectClass = 'checkmark-incorrect fa fa-times';
if (data.result.correct) {
correctness.removeClass(incorrectClass);
correctness.addClass(correctClass);
} else {
correctness.removeClass(correctClass);
correctness.addClass(incorrectClass);
}
$('.status-message', element).text(data.result.msg);
}
function checkAnswer(vectordraw) {
if (checkXHR) {
checkXHR.abort();
}
var state = getInput(vectordraw);
checkXHR = $.post(checkHandlerUrl, JSON.stringify(state))
.success(updateStatus);
}
// Initialization logic
// Initialize exercise.
// Defer it so that we don't try to initialize it for a hidden element (in Studio);
// JSXGraph has problems rendering a board if the containing element is hidden.
window.setTimeout(function() {
var vectordraw = new VectorDraw('vectordraw', init_args.settings);
// Load user state
if (!_.isEmpty(init_args.user_state)) {
vectordraw.setState(init_args.user_state);
updateStatus(init_args.user_state);
}
// Set up click handlers
$('.action .check', element).on('click', function(e) { checkAnswer(vectordraw); });
}, 0);
}
function VectorDrawXBlockEdit(runtime, element, init_args) {
'use strict';
var VectorDraw = function(element_id, settings) {
this.board = null;
this.dragged_vector = null;
this.selectedVector = null;
this.drawMode = false;
this.wasUsed = false;
this.resultMode = false;
this.settings = settings;
this.numberOfVectors = this.settings.vectors.length;
this.editableProperties = ['name', 'label', 'tail', 'length', 'angle'];
this.checks = [
'tail', 'tail_x', 'tail_y', 'tip', 'tip_x', 'tip_y', 'coords', 'length', 'angle'
];
this.element = $('#' + element_id, element);
this.element.on('click', '.controls .add-vector', this.onAddVector.bind(this));
this.element.on('click', '.controls .result-mode', this.onEditResult.bind(this));
this.element.on('change', '.menu .element-list-edit', this.onEditStart.bind(this));
this.element.on('click', '.menu .update', this.onEditSubmit.bind(this));
this.element.on('click', '.menu .remove', this.onRemoveVector.bind(this));
// Prevents default image drag and drop actions in some browsers.
this.element.on('mousedown', '.jxgboard image', function(evt) { evt.preventDefault(); });
this.discardStaleData();
this.render();
};
VectorDraw.prototype.discardStaleData = function() {
// If author removed or renamed vectors via the "Vectors" field
// (without making necessary adjustments in "Expected results" field)
// discard stale information about expected positions and checks
var vectorData = JSON.parse(fieldEditor.getContents('vectors')),
vectorNames = this.getVectorNames(vectorData);
var isStale = function(key) { return !_.contains(vectorNames, key); };
this.settings.expected_result_positions = _.omit(this.settings.expected_result_positions, isStale);
this.settings.expected_result = _.omit(this.settings.expected_result, isStale);
};
VectorDraw.prototype.render = function() {
// Assign the jxgboard element a random unique ID,
// because JXG.JSXGraph.initBoard needs it.
this.element.find('.jxgboard').prop('id', _.uniqueId('jxgboard'));
this.createBoard();
};
VectorDraw.prototype.createBoard = function() {
var id = this.element.find('.jxgboard').prop('id'),
self = this;
this.board = JXG.JSXGraph.initBoard(id, {
keepaspectratio: true,
boundingbox: this.settings.bounding_box,
axis: this.settings.axis,
showCopyright: false,
showNavigation: this.settings.show_navigation
});
function getImageRatio(bg, callback) {
var img = new Image();
$(img).load(function() {
var ratio = this.height / this.width;
callback(bg, ratio);
}).attr({
src: bg.src
});
}
function drawBackground(bg, ratio) {
var height = (bg.height) ? bg.height : bg.width * ratio;
var coords = (bg.coords) ? bg.coords : [-bg.width/2, -height/2];
var image = self.board.create('image', [bg.src, coords, [bg.width, height]], {fixed: true});
$(image.rendNode).attr('alt', bg.description);
}
if (this.settings.background) {
if (this.settings.background.height) {
drawBackground(this.settings.background);
}
else {
getImageRatio(this.settings.background, drawBackground);
}
}
_.each(this.settings.points, function(point, idx) {
this.renderPoint(idx);
}, this);
_.each(this.settings.vectors, function(vec, idx) {
this.renderVector(idx);
}, this);
this.board.on('down', this.onBoardDown.bind(this));
this.board.on('move', this.onBoardMove.bind(this));
this.board.on('up', this.onBoardUp.bind(this));
};
VectorDraw.prototype.renderPoint = function(idx, coords) {
var point = this.settings.points[idx];
var coords = coords || point.coords;
var board_object = this.board.elementsByName[point.name];
if (board_object) {
// If the point is already rendered, only update its coordinates.
board_object.setPosition(JXG.COORDS_BY_USER, coords);
return;
}
this.board.create('point', coords, point.style);
};
VectorDraw.prototype.getVectorNames = function(vectorData) {
if (vectorData) {
return _.pluck(vectorData, 'name');
}
return _.pluck(this.settings.vectors, 'name');
};
VectorDraw.prototype.getVectorCoordinates = function(vec) {
var coords = vec.coords;
if (!coords) {
var tail = vec.tail || [0, 0];
var length = 'length' in vec ? vec.length : 5;
var angle = 'angle' in vec ? vec.angle : 30;
var radians = angle * Math.PI / 180;
var tip = [
tail[0] + Math.cos(radians) * length,
tail[1] + Math.sin(radians) * length
];
coords = [tail, tip];
}
return coords;
};
VectorDraw.prototype.renderVector = function(idx, coords) {
var vec = this.settings.vectors[idx];
coords = coords || this.getVectorCoordinates(vec);
// If this vector is already rendered, only update its coordinates.
var board_object = this.board.elementsByName[vec.name];
if (board_object) {
board_object.point1.setPosition(JXG.COORDS_BY_USER, coords[0]);
board_object.point2.setPosition(JXG.COORDS_BY_USER, coords[1]);
return;
}
var style = vec.style;
var tail = this.board.create('point', coords[0], {
name: vec.name,
size: style.pointSize,
fillColor: style.pointColor,
strokeColor: style.pointColor,
withLabel: false,
fixed: (vec.type === 'arrow' | vec.type === 'vector'),
showInfoBox: false
});
var tip = this.board.create('point', coords[1], {
name: style.label || vec.name,
size: style.pointSize,
fillColor: style.pointColor,
strokeColor: style.pointColor,
withLabel: true,
showInfoBox: false
});
// Not sure why, but including labelColor in attributes above doesn't work,
// it only works when set explicitly with setAttribute.
tip.setAttribute({labelColor: style.labelColor});
tip.label.setAttribute({fontsize: 18, highlightStrokeColor: 'black'});
var line_type = (vec.type === 'vector') ? 'arrow' : vec.type;
var line = this.board.create(line_type, [tail, tip], {
name: vec.name,
strokeWidth: style.width,
strokeColor: style.color
});
// a11y
var lineElement = $(line.rendNode);
var lineID = lineElement.attr("id");
var titleID = lineID + "-title";
var titleElement = $("<title>", {"id": titleID, "text": vec.name});
lineElement.append(titleElement);
lineElement.attr("aria-labelledby", titleID);
var descID = lineID + "-desc";
var descElement = $("<desc>").attr("id", descID).text(vec.description);
lineElement.append(descElement);
lineElement.attr("aria-describedby", descID);
return line;
};
VectorDraw.prototype.getEditMenuOption = function(type, idx) {
return this.element.find('.menu .element-list-edit option[value=' + type + '-' + idx + ']');
};
VectorDraw.prototype.onAddVector = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// Add vector that starts at center of board and has a predefined length and angle
var defaultCoords = [[0, 0], [0, 3]],
defaultVector = this.getDefaultVector(defaultCoords);
this.settings.vectors.push(defaultVector);
var lastIndex = this.numberOfVectors - 1,
vector = this.renderVector(lastIndex);
this.addEditMenuOption(defaultVector.name, lastIndex);
this.updateVectorProperties(vector);
};
VectorDraw.prototype.addEditMenuOption = function(vectorName, idx) {
// Find dropdown for selecting vector to edit
var editMenu = this.element.find('.menu .element-list-edit');
// Remove current selection(s)
editMenu.find('option').attr('selected', false);
// Create option for newly added vector
var newOption = $('<option>')
.attr('value', 'vector-' + idx)
.attr('data-vector-name', vectorName)
.text(vectorName);
// Append option to dropdown
editMenu.append(newOption);
// Select newly added option
newOption.attr('selected', true);
};
VectorDraw.prototype.onRemoveVector = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// Remove selected vector from board
var vectorName = $('.element-list-edit', element).find('option:selected').data('vector-name');
var boardObject = this.board.elementsByName[vectorName];
this.board.removeAncestors(boardObject);
// Mark vector as "deleted" so it will be removed from "vectors" field on save
var vectorSettings = this.getVectorSettingsByName(String(vectorName));
vectorSettings.deleted = true;
// Remove entry that corresponds to selected vector from menu for selecting vector to edit
var idx = _.indexOf(this.settings.vectors, vectorSettings),
editOption = this.getEditMenuOption("vector", idx);
editOption.remove();
// Discard information about expected position (if any)
delete this.settings.expected_result_positions[vectorName];
// Discard information about expected result (if any)
delete this.settings.expected_result[vectorName];
// Reset input fields for vector properties to default values
this.resetVectorProperties();
// Reset selected vector
this.selectedVector = null;
// Hide message about pending changes
$('.vector-prop-update .update-pending', element).hide();
};
VectorDraw.prototype.onEditResult = function(evt) {
// Switch to result mode
this.resultMode = true;
// Save vector positions
this.settings.vectors = this.getState(); // Discards vectors that were removed from board
// Vector positions saved, so hide message about pending changes
this.selectedVector = null;
$('.vector-prop-update .update-pending', element).hide();
// Update vector positions using positions from expected result
var expectedResultPositions = this.settings.expected_result_positions;
if (!_.isEmpty(expectedResultPositions)) {
_.each(this.settings.vectors, function(vec) {
var vectorName = vec.name,
resultPosition = expectedResultPositions[vectorName];
if (resultPosition) {
var resultTail = resultPosition.tail,
resultTip = resultPosition.tip,
boardObject = this.board.elementsByName[vectorName];
boardObject.point1.setPosition(JXG.COORDS_BY_USER, resultTail);
boardObject.point2.setPosition(JXG.COORDS_BY_USER, resultTip);
}
}, this);
this.board.update();
}
// Hide or disable buttons for operations that are specific to defining initial state
$(evt.currentTarget).prop('disabled', true);
$('.add-vector', element).css('visibility', 'hidden');
$('.vector-remove button').hide();
// Reset vector properties to ensure a clean slate
this.resetVectorProperties();
// Show controls for opting in and out of checks
$('.checks', element).show();
};
VectorDraw.prototype.resetVectorProperties = function(vector) {
// Reset dropdown for selecting vector to default value
$('.element-list-edit option[value="-"]', element).attr('selected', true);
// Reset input fields and disable them
// (users should not be able to interact with them unless a vector is selected)
_.each(this.editableProperties, function(propName) {
$('.vector-prop-' + propName + ' input', element).prop('disabled', true).val('');
});
// Disable buttons
$('.vector-properties button').prop('disabled', true);
};
VectorDraw.prototype.getMouseCoords = function(evt) {
var i = evt[JXG.touchProperty] ? 0 : undefined;
var c_pos = this.board.getCoordsTopLeftCorner(evt, i);
var abs_pos = JXG.getPosition(evt, i);
var dx = abs_pos[0] - c_pos[0];
var dy = abs_pos[1] - c_pos[1];
return new JXG.Coords(JXG.COORDS_BY_SCREEN, [dx, dy], this.board);
};
VectorDraw.prototype.getVectorForObject = function(obj) {
if (obj instanceof JXG.Line) {
return obj;
}
if (obj instanceof JXG.Text) {
return this.getVectorForObject(obj.element);
}
if (obj instanceof JXG.Point) {
return _.find(obj.descendants, function (d) { return (d instanceof JXG.Line); });
}
return null;
};
VectorDraw.prototype.getVectorSettingsByName = function(name) {
return _.find(this.settings.vectors, function(vec) {
return vec.name === name;
});
};
VectorDraw.prototype.updateVectorProperties = function(vector) {
var vec_settings = this.getVectorSettingsByName(vector.name);
var x1 = vector.point1.X(),
y1 = vector.point1.Y(),
x2 = vector.point2.X(),
y2 = vector.point2.Y();
var length = vec_settings.length_factor * Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
var angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180) - vec_settings.base_angle) % 360;
if (angle < 0) {
angle += 360;
}
// If user selected different vector, hide message about pending changes
if (this.selectedVector && vector.name !== this.selectedVector.name) {
$('.vector-prop-update .update-pending', element).hide();
}
// Update selected vector
this.selectedVector = vector;
// Update menu for selecting vector to edit
this.element.find('.menu .element-list-edit option').attr('selected', false);
var idx = _.indexOf(this.settings.vectors, vec_settings),
editOption = this.getEditMenuOption("vector", idx);
editOption.attr('selected', true);
// Update properties
$('.vector-prop-name input', this.element).val(vector.name);
$('.vector-prop-label input', this.element).val(vec_settings.style.label || '');
$('.vector-prop-angle input', this.element).val(angle.toFixed(2));
if (vector.elType !== "line") {
var tailInput = x1.toFixed(2) + ", " + y1.toFixed(2);
var lengthInput = length.toFixed(2);
if (vec_settings.length_units) {
lengthInput += ' ' + vec_settings.length_units;
}
$('.vector-prop-tail input', this.element).val(tailInput);
$('.vector-prop-length', this.element).show();
$('.vector-prop-length input', this.element).val(lengthInput);
}
else {
$('.vector-prop-length', this.element).hide();
}
// Enable input fields
$('.vector-properties input').prop('disabled', false);
// Enable buttons
$('.vector-properties button').prop('disabled', false);
};
VectorDraw.prototype.updateChecks = function(vector) {
var expectedResult = this.settings.expected_result[vector.name] || {};
_.each(this.checks, function(check) {
var checkElement = $('#check-' + check, element);
// Update checkbox
if (expectedResult[check]) {
checkElement.find('input[type="checkbox"]').prop('checked', true);
} else {
checkElement.find('input[type="checkbox"]').prop('checked', false);
}
// Update tolerance
var tolerance = expectedResult[check + '_tolerance'];
if (tolerance) {
checkElement.find('input[type="number"]').val(tolerance.toFixed(1));
} else {
var defaultTolerance = check === 'angle' ? 2.0 : 1.0;
checkElement.find('input[type="number"]').val(defaultTolerance.toFixed(1));
}
});
};
VectorDraw.prototype.saveExpectedPosition = function(vectorName, coords, length, angle) {
var expectedPosition = {
coords: coords,
tail: coords[0],
tip: coords[1],
tail_x: coords[0][0],
tail_y: coords[0][1],
tip_x: coords[1][0],
tip_y: coords[1][1],
length: length,
angle: angle
};
this.settings.expected_result_positions[vectorName] = expectedPosition;
};
VectorDraw.prototype.saveChecks = function(vectorName) {
var expectedResult = {};
_.each(this.checks, function(check) {
var checkElement = $('#check-' + check, element);
if (checkElement.find('input[type="checkbox"]').prop('checked')) {
// Insert (or update) check: Need current position of selected vector
expectedResult[check] = this.settings.expected_result_positions[vectorName][check];
// Insert (or update) tolerance
var tolerance = checkElement.find('input[type="number"]').val();
expectedResult[check + '_tolerance'] = parseFloat(tolerance);
}
}, this);
if (_.isEmpty(expectedResult)) { // If author doesn't select any checks,
// assume they also want to skip the presence check
// (which the grader will perform automatically
// for each vector that has an entry in expected_result)
delete this.settings.expected_result[vectorName];
} else {
this.settings.expected_result[vectorName] = expectedResult;
}
};
VectorDraw.prototype.isVectorTailDraggable = function(vector) {
return vector.elType !== 'arrow';
};
VectorDraw.prototype.canCreateVectorOnTopOf = function(el) {
// If the user is trying to drag the arrow of an existing vector, we should not create a new vector.
if (el instanceof JXG.Line) {
return false;
}
// If this is tip/tail of a vector, it's going to have a descendant Line - we should not create a new vector
// when over the tip. Creating on top of the tail is allowed for plain vectors but not for segments.
// If it doesn't have a descendant Line, it's a point from settings.points - creating a new vector is allowed.
if (el instanceof JXG.Point) {
var vector = this.getVectorForObject(el);
if (!vector) {
return el.getProperty('fixed');
} else if (el === vector.point1 && !this.isVectorTailDraggable(vector)) {
return true;
} else {
return false;
}
}
return true;
};
VectorDraw.prototype.objectsUnderMouse = function(coords) {
var filter = function(el) {
return !(el instanceof JXG.Image) && el.hasPoint(coords.scrCoords[1], coords.scrCoords[2]);
};
return _.filter(_.values(this.board.objects), filter);
};
VectorDraw.prototype.getDefaultVector = function(coords) {
this.numberOfVectors += 1;
var name = String(this.numberOfVectors),
description = "Vector " + name;
return {
name: name,
description: description,
coords: coords,
type: "vector",
render: false,
length_factor: 1,
length_units: "",
base_angle: 0,
style: {
pointSize: 1,
pointColor: "red",
width: 4,
color: "blue",
label: null,
labelColor: "black"
}
};
};
VectorDraw.prototype.onBoardDown = function(evt) {
var coords = this.getMouseCoords(evt);
var targetObjects = this.objectsUnderMouse(coords);
if (!targetObjects || _.all(targetObjects, this.canCreateVectorOnTopOf.bind(this))) {
if (this.resultMode) {
return;
}
// Add vector to board
var point_coords = [coords.usrCoords[1], coords.usrCoords[2]];
var defaultVector = this.getDefaultVector([point_coords, point_coords]);
this.settings.vectors.push(defaultVector);
var lastIndex = this.numberOfVectors - 1;
this.drawMode = true;
this.dragged_vector = this.renderVector(lastIndex);
this.addEditMenuOption(defaultVector.name, lastIndex);
}
else {
// Move existing vector around
this.drawMode = false;
var vectorPoint = _.find(targetObjects, this.getVectorForObject.bind(this));
if (vectorPoint) {
this.dragged_vector = this.getVectorForObject(vectorPoint);
this.dragged_vector.point1.setProperty({fixed: false});
this.updateVectorProperties(this.dragged_vector);
if (this.resultMode) {
_.each(['name', 'label'], function(propName) {
$('.vector-prop-' + propName + ' input', element).prop('disabled', true);
});
this.updateChecks(this.dragged_vector);
$('.checks input', element).prop('disabled', false);
}
}
}
};
VectorDraw.prototype.onBoardMove = function(evt) {
if (this.drawMode) {
var coords = this.getMouseCoords(evt);
this.dragged_vector.point2.moveTo(coords.usrCoords);
}
if (this.dragged_vector) {
this.updateVectorProperties(this.dragged_vector);
}
};
VectorDraw.prototype.onBoardUp = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
this.drawMode = false;
if (this.dragged_vector && !this.isVectorTailDraggable(this.dragged_vector)) {
this.dragged_vector.point1.setProperty({fixed: true});
}
this.dragged_vector = null;
};
VectorDraw.prototype.onEditStart = function(evt) {
var vectorName = $(evt.currentTarget).find('option:selected').data('vector-name');
var vectorObject = this.board.elementsByName[vectorName];
this.updateVectorProperties(vectorObject);
if (this.resultMode) {
_.each(['name', 'label'], function(propName) {
$('.vector-prop-' + propName + ' input', element).prop('disabled', true);
});
this.updateChecks(vectorObject);
$('.checks input', element).prop('disabled', false);
}
};
VectorDraw.prototype.onEditSubmit = function(evt) {
if (!this.wasUsed) {
this.wasUsed = true;
}
// About to save changes, so hide message about pending changes
$('.vector-prop-update .update-pending', element).hide();
// Get name of vector that is currently "selected"
var vectorName = String($('.element-list-edit', element).find('option:selected').data('vector-name')),
newValues = {};
// Get values from input fields
_.each(this.editableProperties, function(prop) {
newValues[prop] = $.trim($('.vector-prop-' + prop + ' input', element).val());
});
// Process values
var newName = newValues.name,
newLabel = newValues.label,
newTail = _.map(newValues.tail.split(/ *, */), function(coord) { return parseFloat(coord); }),
newLength = parseFloat(newValues.length),
newAngle = parseFloat(newValues.angle);
// Validate and update values
var vectorNames = this.getVectorNames(),
vectorSettings = this.getVectorSettingsByName(vectorName),
boardObject = this.board.elementsByName[vectorName];
// 1. Update name
if (newName && newName !== vectorName && !_.contains(vectorNames, newName)) {
// Update vector settings
vectorSettings.name = newName;
// Update dropdown for selecting vector to edit
var editOption = $('.menu .element-list-edit option[data-vector-name="' + vectorName + '"]', element);
editOption.data('vector-name', newName);
editOption.text(newName);
// Update board
boardObject.name = newName;
boardObject.point2.name = newName;
this.board.elementsByName[newName] = boardObject;
delete this.board.elementsByName[vectorName];
// Update expected position
var expectedPositions = this.settings.expected_result_positions,
expectedPosition = expectedPositions[vectorName];
if (expectedPosition) {
expectedPositions[newName] = expectedPosition;
delete expectedPositions[vectorName];
}
// Update expected result
var expectedResults = this.settings.expected_result,
expectedResult = expectedResults[vectorName];
if (expectedResult) {
expectedResults[newName] = expectedResult;
delete expectedResults[vectorName];
}
} else {
$('.vector-prop-name input', element).val(vectorName);
}
// 2. Update label
if (newLabel) {
vectorSettings.style.label = newLabel;
boardObject.point2.name = newLabel; // Always prefer label for labeling vector on board
} else {
vectorSettings.style.label = null;
boardObject.point2.name = newName || vectorName; // ... but fall back on name if label was removed
}
// 3. Update tail, length, angle
var values = [newTail[0], newTail[1], newLength, newAngle];
if (!_.some(values, Number.isNaN)) {
$('.vector-prop-update .update-error', element).hide();
// Use coordinates of new tail, new length, new angle to calculate new position of tip
var radians = newAngle * Math.PI / 180;
var newTip = [
newTail[0] + Math.cos(radians) * newLength,
newTail[1] + Math.sin(radians) * newLength
];
// Update position of vector
boardObject.point1.setPosition(JXG.COORDS_BY_USER, newTail);
boardObject.point2.setPosition(JXG.COORDS_BY_USER, newTip);
this.board.update();
// If board is in result mode, also save
// - expected position
// - check data
// for "selected" vector
if (this.resultMode) {
this.saveExpectedPosition(vectorName, [newTail, newTip], newLength, newAngle);
this.saveChecks(vectorName);
}
} else {
$('.vector-prop-update .update-error', element).show();
}
};
VectorDraw.prototype.getVectorCoords = function(name) {
var object = this.board.elementsByName[name];
return {
tail: [object.point1.X(), object.point1.Y()],
tip: [object.point2.X(), object.point2.Y()]
};
};
VectorDraw.prototype.getState = function() {
var vectors = [];
_.each(this.settings.vectors, function(vec) {
if (vec.deleted) {
return;
}
var coords = this.getVectorCoords(vec.name),
tail = coords.tail,
tip = coords.tip,
x1 = tail[0],
y1 = tail[1],
x2 = tip[0],
y2 = tip[1];
// Update coordinates
vec.coords = [tail, tip];
vec.tail = tail;
vec.tip = tip;
// Update length, angle
vec.length = Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
// Update angle
vec.angle = ((Math.atan2(y2-y1, x2-x1)/Math.PI*180)) % 360;
vectors.push(vec);
}, this);
return vectors;
};
// Initialization logic
// Initialize functionality for non-WYSIWYG editing functionality
var fieldEditor = StudioEditableXBlockMixin(runtime, element);
// Initialize WYSIWYG editor
var vectordraw = new VectorDraw('vectordraw', init_args.settings);
// Set up click handlers
$('.save-button', element).on('click', function(e) {
e.preventDefault();
var data = {};
if (vectordraw.wasUsed) {
// If author edited both initial state and result,
// vectordraw.settings.vectors corresponds to state vectors were in
// when author switched to result mode
data.vectors = vectordraw.resultMode ? vectordraw.settings.vectors : vectordraw.getState();
data.expected_result_positions = vectordraw.settings.expected_result_positions;
data.expected_result = vectordraw.settings.expected_result;
}
fieldEditor.save(data);
});
$('.cancel-button', element).on('click', function(e) {
e.preventDefault();
fieldEditor.cancel();
});
$('.info-button', element).on('click', function(e) {
e.preventDefault();
$('#wysiwyg-description', element).toggle();
});
$('input', element).on('change', function(e) {
$('.update-error').hide();
$('.update-pending').show();
});
}
{% load i18n %}
<div class="vectordraw_block">
<h2>{{ self.display_name }}</h2>
{% if self.description %}
<div class="vectordraw-description">
{{ self.description|safe }}
</div>
{% endif %}
<div id="vectordraw">
<div class="menu" style="width: {{ self.width }}px;">
<div class="controls">
<label class="sr" for="element-list">{% trans "Select element to add to board" %}</label>
<select id="element-list" class="element-list-add">
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}">
{{ vector.description }}
</option>
{% endfor %}
{% for point in self.get_points %}
{% if not point.fixed %}
<option value="point-{{ forloop.counter0 }}">
{{ point.description }}
</option>
{% endif %}
{% endfor %}
</select>
<button class="add-vector">
{{ self.add_vector_label }}
</button>
<button class="reset">
<span class="reset-label" aria-hidden="true">{% trans "Reset" %}</span>
<span class="sr">{% trans "Reset board to initial state" %}</span>
</button>
<button class="redo" title="{% trans 'Redo' %}">
<span class="redo-label fa fa-repeat" aria-hidden="true"></span>
<span class="sr">{% trans "Redo last action" %}</span>
</button>
<button class="undo" title="{% trans 'Undo' %}">
<span class="undo-label fa fa-undo" aria-hidden="true"></span>
<span class="sr">{% trans "Undo last action" %}</span>
</button>
</div>
{% if self.show_vector_properties %}
<div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list">
<div class="row">
<div class="vector-prop vector-prop-name">
<span id="vector-prop-name-label">
{% trans "name" %}:
</span>
<select class="element-list-edit" aria-labelledby="vector-prop-name-label">
<option value="-" selected="selected" disabled="disabled">-</option>
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}">
{{ vector.name }}
</option>
{% endfor %}
{% for point in self.get_points %}
{% if not point.fixed %}
<option value="point-{{ forloop.counter0 }}">
{{ point.name }}
</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-tail">
<span id="vector-prop-tail-label">
{% trans "tail position" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-tail-label">
</div>
<div class="vector-prop vector-prop-length">
<span id="vector-prop-length-label">
{% trans "length" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-length-label">
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-angle">
<span id="vector-prop-angle-label">
{% trans "angle" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-angle-label">
</div>
<div class="vector-prop vector-prop-slope">
<span id="vector-prop-slope-label">
{% trans "slope" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-slope-label">
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-update">
<button class="update" disabled="disabled">
<span class="update-label" aria-hidden="true">{% trans "Update" %}</span>
<span class="sr">{% trans "Update properties of selected element" %}</span>
</button>
<span class="update-error">{% trans "Invalid input." %}</span>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;"
aria-live="polite"></div>
</div>
<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" aria-hidden="true">{% trans "Check" %}</span>
<span class="sr">{% trans "Check your answer" %}</span>
</button>
</div>
</div>
{% load i18n %}
<div class="editor-with-buttons">
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<ul class="list-input settings-list">
{% for field in fields %}
<li class="field comp-setting-entry metadata_entry {% if field.is_set %}is-set{% endif %}"
data-field-name="{{field.name}}"
data-default="{% if field.type == 'boolean' %}{{ field.default|yesno:'1,0' }}{% else %}{{ field.default|default_if_none:"" }}{% endif %}"
data-cast="{{field.type}}">
<div class="wrapper-comp-setting{% if field.type == "set" %} metadata-list-enum {%endif%}">
<label class="label setting-label" for="xb-field-edit-{{field.name}}">{{field.display_name}}</label>
{% if field.type == "boolean" %}
<select class="field-data-control"
id="xb-field-edit-{{field.name}}"
aria-describedby="{{field.name}}-help">
<option value="1" {% if field.value %}selected{% endif %}>
{% trans "True" %} {% if field.default %}&nbsp;&nbsp;&nbsp;&nbsp;({% trans "Default" %}){% endif %}
</option>
<option value="0" {% if not field.value %}selected{% endif %}>
{% trans "False" %} {% if not field.default %}&nbsp;&nbsp;&nbsp;&nbsp;({% trans "Default" %}){% endif %}
</option>
</select>
{% elif field.type == "string" %}
<input type="text"
class="field-data-control"
id="xb-field-edit-{{field.name}}"
value="{{field.value|default_if_none:""}}"
aria-describedby="{{field.name}}-help">
{% elif field.type == "integer" or field.type == "float" %}
<input type="number"
class="field-data-control"
id="xb-field-edit-{{field.name}}"
{% if field.step %} step="{{field.step}}" {% elif field.type == "integer" %} step=1 {% endif %}
{% if field.max %} max="{{field.max}}" {% endif %}
{% if field.min %} min="{{field.min}}" {% endif %}
value="{{field.value|default_if_none:""}}"
aria-describedby="{{field.name}}-help">
{% elif field.type == "text" or field.type == "html" %}
<textarea class="field-data-control"
data-field-name="{{field.name}}"
id="xb-field-edit-{{field.name}}"
aria-describedby="{{field.name}}-help"
rows=10 cols=70>{{field.value}}</textarea>
{% else %}
{% trans "Unsupported field type. This setting cannot be edited." %}
{% endif %}
{% if field.allow_reset %}
<button class="action setting-clear {% if field.is_set %}active{%else%}inactive{% endif %}"
type="button"
name="setting-clear"
value="{% trans "Clear" %}"
data-tooltip="{% trans "Clear" %}">
<i class="icon fa fa-undo"></i><span class="sr">{% trans "Clear Value" %}</span>
</button>
{% endif %}
</div>
{% if field.help %}
<span id="{{field.name}}-help" class="tip setting-help"> {{ field.help|safe }} </span>
{% endif %}
</li>
{% endfor %}
<li>
<!-- WYSIWYG editor -->
<div class="vectordraw_block vectordraw_edit_block">
<h2 aria-describedby="wysiwyg-description">
WYSIWYG Editor
<button class="info-button" title="Info">
<span class="info-label fa fa-question" aria-hidden="true"></span>
<span class="sr">
{# Translators: WYSIWYG stands for What You See Is What You Get. When using a WYSIWYG editor, the content being edited is shown in a way that closely corresponds to the finished product. #}
{% trans "Toggle information about how to use the WYSIWYG editor" %}
</span>
</button>
</h2>
<div id="wysiwyg-description">
<p>
{% blocktrans %}
Instead of using the "Vectors" and "Expected result" fields above
to define or modify the set of working vectors and expected result for this exercise,
you can also use the board below.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
To add a vector, left-click the board where you want the vector to originate.
Keep holding down the left mouse button and drag your mouse pointer across the board
to achieve the desired length and angle for the vector.
Alternatively, you can click "Create vector", which will add a new vector
that starts at the center of the board and has a predefined length (3) and angle (90).
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
To modify the position of an existing vector, left-click it, hold down the left mouse button,
and move your mouse pointer across the board. To modify length and/or angle,
left-click the tip of the vector and drag your mouse pointer across the board.
Alternatively, you can select an existing vector from the dropdown menu,
modify its tail position, length, and angle by changing the values
in the corresponding input fields, and click "Update" to update its position on the board.
You can also modify the name and label of a vector using this technique.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
To remove an existing vector, left-click it or select it from the dropdown menu,
then click "Remove".
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
When you are done defining the set of working vectors, click "Edit result"
to switch the editor to a mode that will allow you to define the expected result for this exercise.
In this mode you can operate on vectors as described above but you can not add or remove vectors,
and you may not change the name and label of a selected vector.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
To define the expected result for the exercise, place each vector where it would be located
in a correct solution. When a vector is selected and positioned correctly,
use the menu to the right of the board to select the checks that you would like the grader
to perform for this vector, then click "Update" to save expected position and associated checks
for the vector. Note that if you do not select any checks for a given vector,
no checks at all will be performed for it during grading (i.e., the grader will skip a presence check).
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
Finally, note that if you make changes using the board below, any changes you made
via the "Vectors" and "Expected results" fields above will be overwritten
when you save the settings for this exercise by clicking the "Save" button
at the bottom of this dialog.
{% endblocktrans %}
</p>
</div>
<div id="vectordraw">
<div class="menu" style="width: {{ self.width }}px;">
<div class="controls">
<button class="add-vector">{% trans "Create vector" %}</button>
<button class="result-mode">{% trans "Edit result" %}</button>
</div>
<div class="vector-properties" aria-live="polite">
<h3>{{ self.vector_properties_label }}</h3>
<div class="vector-prop-list">
<div class="row">
<div class="vector-prop vector-select">
<span id="vector-select-label">
{% trans "vector" %}:
</span>
<select class="element-list-edit" aria-labelledby="vector-select-label">
<option value="-" selected="selected" disabled="disabled">-</option>
{% for vector in self.get_vectors %}
<option value="vector-{{ forloop.counter0 }}" data-vector-name="{{ vector.name }}">
{{ vector.name }}
</option>
{% endfor %}
</select>
</div>
<div class="vector-prop vector-prop-name">
<span id="vector-prop-name-label">
{% trans "name" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-name-label">
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-label">
<span id="vector-prop-label-label">
{% trans "label" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-label-label">
</div>
<div class="vector-prop vector-prop-tail">
<span id="vector-prop-tail-label">
{% trans "tail position" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-tail-label">
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-length">
<span id="vector-prop-length-label">
{% trans "length" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-length-label">
</div>
<div class="vector-prop vector-prop-angle">
<span id="vector-prop-angle-label">
{% trans "angle" %}:
</span>
<input type="text" disabled="disabled" placeholder="-" aria-labelledby="vector-prop-angle-label">
</div>
</div>
<div class="row">
<div class="vector-prop vector-prop-update">
<button class="update" disabled="disabled">
<span class="update-label" aria-hidden="true">{% trans "Update" %}</span>
<span class="sr">{% trans "Update properties of selected element" %}</span>
</button>
<span class="update-pending">
{% trans "Unsaved changes." %}
</span>
<span class="update-error">{% trans "Invalid input." %}</span>
</div>
<div class="vector-prop vector-remove">
<button class="remove" disabled="disabled">
<span class="remove-label" aria-hidden="true">{% trans "Remove" %}</span>
<span class="sr">{% trans "Remove selected element" %}</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="jxgboard"
style="width: {{ self.width }}px; height: {{ self.height }}px;"
tabindex="0">
</div>
<div class="checks" style="height: {{ self.height }}px;">
<h3>{% trans "Checks" %}</h3>
<div class="check" id="check-tail">
<div class="row">
<span id="tail-check-label">
{% trans "check tail" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tail-check-label">
</div>
<div class="row">
<span id="tail-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tail-tolerance-label">
</div>
</div>
<div class="check" id="check-tip">
<div class="row">
<span id="tip-check-label">
{% trans "check tip" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tip-check-label">
</div>
<div class="row">
<span id="tip-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tip-tolerance-label">
</div>
</div>
<div class="check" id="check-tail_x">
<div class="row">
<span id="tail-x-check-label">
{% trans "check tail(x)" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tail-x-check-label">
</div>
<div class="row">
<span id="tail-x-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tail-x-tolerance-label">
</div>
</div>
<div class="check" id="check-tail_y">
<div class="row">
<span id="tail-y-check-label">
{% trans "check tail(y)" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tail-y-check-label">
</div>
<div class="row">
<span id="tail-y-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tail-y-tolerance-label">
</div>
</div>
<div class="check" id="check-tip_x">
<div class="row">
<span id="tip-x-check-label">
{% trans "check tip(x)" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tip-x-check-label">
</div>
<div class="row">
<span id="tip-x-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tip-x-tolerance-label">
</div>
</div>
<div class="check" id="check-tip_y">
<div class="row">
<span id="tip-y-check-label">
{% trans "check tip(y)" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="tip-y-check-label">
</div>
<div class="row">
<span id="tip-y-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="tip-y-tolerance-label">
</div>
</div>
<div class="check" id="check-coords">
<div class="row">
<span id="coords-check-label">
{% trans "check coords" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="coords-check-label">
</div>
<div class="row">
<span id="coords-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="coords-tolerance-label">
</div>
</div>
<div class="check" id="check-length">
<div class="row">
<span id="length-check-label">
{% trans "check length" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="length-check-label">
</div>
<div class="row">
<span id="length-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="length-tolerance-label">
</div>
</div>
<div class="check" id="check-angle">
<div class="row">
<span id="angle-check-label">
{% trans "check angle" %}:
</span>
<input type="checkbox" disabled="disabled" aria-labelledby="angle-check-label">
</div>
<div class="row">
<span id="angle-tolerance-label">
{% trans "tolerance" %}:
</span>
<input type="number" disabled="disabled" min="0" step="0.1" placeholder="-" aria-labelledby="angle-tolerance-label">
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
<div class="xblock-actions">
<ul>
<li class="action-item">
<a href="#" class="button action-primary save-button">{% trans "Save" %}</a>
</li>
<li class="action-item">
<a href="#" class="button cancel-button">{% trans "Cancel" %}</a>
</li>
</ul>
</div>
</div>
<vertical_demo>
<vectordraw url_name="vectordraw_example"
description="&lt;p&gt;A box remains stationary on an inclined plane at an angle of 20 degrees with respect to the x-axis. Draw all appropriate vectors that define the force diagram for this system.&lt;/p&gt;&lt;p&gt;&lt;em&gt;Be sure that the &quot;tail&quot; of each vector starts at the center of mass of the box.&lt;/em&gt;&lt;/p&gt;"
width="550"
height="450"
bounding_box_size="8"
background_url="https://github.com/open-craft/jsinput-vectordraw/raw/master/Notes_and_Examples/2_boxIncline_multiVector/box_on_incline.png"
background_width="20"
vectors="[{&quot;name&quot;:&quot;N&quot;,&quot;description&quot;:&quot;Normal force - N&quot;,&quot;tail&quot;:[2,2],&quot;length&quot;:4,&quot;angle&quot;:45,&quot;render&quot;:true},{&quot;name&quot;:&quot;f&quot;,&quot;description&quot;:&quot;Friction - f&quot;,&quot;coords&quot;:[[-2,-5],[-1,-3]],&quot;render&quot;:false},{&quot;name&quot;:&quot;g&quot;,&quot;description&quot;:&quot;Gravity - g&quot;,&quot;tail&quot;:[0,1.5],&quot;length&quot;:5,&quot;angle&quot;:-75,&quot;render&quot;:false}]"
points="[{&quot;name&quot;:&quot;cm&quot;,&quot;coords&quot;:[-0.6,0.4],&quot;fixed&quot;:false,&quot;description&quot;:&quot;A simple point&quot;}]"
expected_result="{&quot;N&quot;:{&quot;angle&quot;:110,&quot;angle_tolerance&quot;:2,&quot;tail&quot;:[-0.6,0.4],&quot;tail_tolerance&quot;:0.5},&quot;g&quot;:{&quot;angle&quot;:270,&quot;angle_tolerance&quot;:2,&quot;tail&quot;:[-0.6,0.4],&quot;tail_tolerance&quot;:0.5},&quot;f&quot;:{&quot;angle&quot;:20,&quot;angle_tolerance&quot;:2,&quot;tail&quot;:[-0.6,0.4],&quot;tail_tolerance&quot;:0.5}}"
/>
</vertical_demo>
"""
This module contains utility functions for the Vector Drawing XBlock.
"""
def get_doc_link(section, link_text="here"):
"""
Return link to specific `section` of README for Vector Drawing exercises.
"""
return (
'<a href="https://github.com/open-craft/jsinput-vectordraw#{section}" target="_blank">'
'{link_text}'
'</a>'
).format(section=section, link_text=link_text)
"""An XBlock that allows course authors to define vector drawing exercises."""
import json
import logging
from xblock.core import XBlock
from xblock.exceptions import JsonHandlerError
from xblock.fields import Scope, Boolean, Dict, Float, Integer, String
from xblock.fragment import Fragment
from xblock.validation import ValidationMessage
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable import StudioEditableXBlockMixin
try:
# Used to detect if we're in the workbench so we can add Underscore.js
from workbench.runtime import WorkbenchRuntime
except ImportError:
WorkbenchRuntime = False # pylint: disable=invalid-name
from .grader import Grader
from .utils import get_doc_link
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
log = logging.getLogger(__name__) # pylint: disable=invalid-name
class VectorDrawXBlock(StudioEditableXBlockMixin, XBlock):
"""
An XBlock that allows course authors to define vector drawing exercises.
"""
# Content
display_name = String(
display_name="Title (display name)",
help="Title to display",
default="Vector Drawing",
scope=Scope.content
)
description = String(
display_name="Description",
help="Exercise description displayed above vector drawing box",
default="",
multiline_editor="html",
resettable_editor=False,
scope=Scope.content
)
width = Integer(
display_name="Width",
help="The width of the board in pixels",
default=550,
scope=Scope.content
)
height = Integer(
display_name="Height",
help="The height of the board in pixels",
default=400,
scope=Scope.content
)
bounding_box_size = Integer(
display_name="Bounding box size",
help=(
"Defines the bounding box height of the graph area. "
"The bounding box width is calculated from the width/height ratio."
),
default=10,
scope=Scope.content
)
axis = Boolean(
display_name="Show axis",
help=(
"Show the graph axis. "
"Will also show grid lines (but note that the background image might cover them). "
"Enabling this option makes the exercise more accessible for users "
"relying on the keyboard for manipulating vectors."
),
default=True,
scope=Scope.content
)
show_navigation = Boolean(
display_name="Show navigation",
help="Show navigation arrows and zoom controls",
default=False,
scope=Scope.content
)
show_vector_properties = Boolean(
display_name="Show vector properties",
help="Show box detailing vector properties",
default=True,
scope=Scope.content
)
show_slope_for_lines = Boolean(
display_name="Show slope for lines",
help="If True, slope will be shown for line objects.",
default=False,
scope=Scope.content
)
add_vector_label = String(
display_name="Add vector label",
help="Label for button that allows to add vectors to the board",
default="Add Selected Force",
scope=Scope.content
)
vector_properties_label = String(
display_name="Vector properties label",
help="Label for box that displays vector properties",
default="Vector Properties",
scope=Scope.content
)
background_url = String(
display_name="Background URL",
help="URL for background image",
default="",
scope=Scope.content
)
background_width = Integer(
display_name="Background width",
help="Width of background image",
default=0,
scope=Scope.content
)
background_height = Integer(
display_name="Background height",
help="Height of background image",
default=0,
scope=Scope.content
)
background_description = String(
display_name="Background description",
help=(
"Please provide a description of the image for non-visual users. "
"The description should provide sufficient information that would allow anyone "
"to solve the problem if the image did not load."
),
default="",
scope=Scope.content
)
vectors = String(
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. "
"See {doc_link} for more information. "
"Note that you can also use the WYSIWYG editor below to create or modify vectors. "
"If you do, any changes you make here will be overwritten by vector data "
"from the WYSIWYG editor when saving."
).format(doc_link=get_doc_link('vectors')),
default="[]",
multiline_editor=True,
resettable_editor=False,
scope=Scope.content
)
points = String(
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. "
"See {doc_link} for more information."
).format(doc_link=get_doc_link('points')),
default="[]",
multiline_editor=True,
resettable_editor=False,
scope=Scope.content
)
expected_result = String(
display_name="Expected result",
help=(
"Defines vector properties for grading. "
"You must specify it as a JSON object where each key is the name of an existing vector "
"and each value is a JSON object containing information about checks to perform "
"and expected values. "
"See {doc_link} for more information. "
"Vectors omitted from this setting are ignored when grading. "
"Note that you can also use the WYSIWYG editor below to opt in and out of checks "
"for individual vectors. "
"If you use the WYSIWYG editor at all, any changes you make here "
"will be overwritten when saving."
).format(doc_link=get_doc_link('expected_result')),
default="{}",
multiline_editor=True,
resettable_editor=False,
scope=Scope.content
)
custom_checks = String(
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.'
),
default="[]",
multiline_editor=True,
resettable_editor=False,
scope=Scope.content
)
weight = Float(
display_name="Weight",
default=1,
scope=Scope.settings,
enforce_type=True
)
# Dictionary that keeps track of vector positions for correct answer;
# treated as an editable field but hidden from author in Studio
# since changes to it are implicit
expected_result_positions = Dict(scope=Scope.content)
# User state
# Dictionary containing vectors and points present on the board when user last clicked "Check",
# as well as checks to perform in order to obtain grade
answer = Dict(scope=Scope.user_state)
# Dictionary that represents result returned by the grader for the most recent answer;
# contains info about correctness of answer and feedback message
result = Dict(scope=Scope.user_state)
editable_fields = (
'display_name',
'description',
'width',
'height',
'bounding_box_size',
'axis',
'show_navigation',
'show_vector_properties',
'show_slope_for_lines',
'add_vector_label',
'vector_properties_label',
'background_url',
'background_width',
'background_height',
'background_description',
'vectors',
'points',
'expected_result',
'expected_result_positions',
'custom_checks'
)
has_score = True
@property
def settings(self):
"""
Return settings for this exercise.
"""
width_scale = self.width / float(self.height)
box_size = self.bounding_box_size
bounding_box = [-box_size*width_scale, box_size, box_size*width_scale, -box_size]
return {
'width': self.width,
'height': self.height,
'bounding_box': bounding_box,
'axis': self.axis,
'show_navigation': self.show_navigation,
'show_vector_properties': self.show_vector_properties,
'show_slope_for_lines': self.show_slope_for_lines,
'add_vector_label': self.add_vector_label,
'vector_properties_label': self.vector_properties_label,
'background': self.background,
'vectors': self.get_vectors,
'points': self.get_points,
'expected_result': self.get_expected_result,
'expected_result_positions': self.expected_result_positions,
}
@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
return user_state
@property
def background(self):
"""
Return information about background to draw for this exercise.
"""
return {
'src': self.background_url,
'width': self.background_width,
'height': self.background_height,
'description': self.background_description,
}
def _get_default_vector(self): # pylint: disable=no-self-use
"""
Return dictionary that represents vector with default values filled in.
"""
return {
'type': 'vector',
'render': False,
'length_factor': 1,
'length_units': '',
'base_angle': 0,
'style': {
'pointSize': 1,
'pointColor': 'red',
'width': 4,
'color': 'blue',
'label': None,
'labelColor': 'black'
}
}
@property
def get_vectors(self):
"""
Return info about vectors belonging to this exercise.
To do this, load vector info from JSON string specified by course author,
and augment it with default values that are required for rendering vectors on the client.
"""
vectors = []
for vector in json.loads(self.vectors):
default_vector = self._get_default_vector()
default_vector_style = default_vector['style']
default_vector_style.update(vector.pop('style', {}))
default_vector.update(vector)
vectors.append(default_vector)
return vectors
def _get_default_point(self): # pylint: disable=no-self-use
"""
Return dictionary that represents point with default values filled in.
"""
return {
'fixed': True, # Default to True for backwards compatibility
'render': True,
'style': {
'size': 1,
'withLabel': False,
'color': 'pink',
'showInfoBox': False
}
}
@property
def get_points(self):
"""
Return info about points belonging to this exercise.
To do this, load point info from JSON string specified by course author,
and augment it with default values that are required for rendering points on the client.
"""
points = []
for point in json.loads(self.points):
default_point = self._get_default_point()
default_point_style = default_point['style']
default_point_style.update(point.pop('style', {}))
default_point.update(point)
default_point_style['name'] = default_point['name']
default_point_style['fixed'] = default_point['fixed']
point_color = default_point_style['color']
default_point_style['strokeColor'] = point_color
default_point_style['fillColor'] = point_color
del default_point_style['color']
points.append(default_point)
return points
@property
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):
"""
The primary view of the VectorDrawXBlock, shown to students
when viewing courses.
"""
context = context or {}
context['self'] = self
fragment = Fragment()
fragment.add_content(loader.render_template('templates/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(self.runtime.local_resource_url(self, 'public/css/vectordraw.css'))
# Workbench doesn't have Underscore.js, so add it:
if WorkbenchRuntime and isinstance(self.runtime, WorkbenchRuntime):
fragment.add_javascript_url(
"//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"
)
fragment.add_javascript_url(
"//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js"
)
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/vectordraw.js')
)
fragment.initialize_js(
'VectorDrawXBlock', {"settings": self.settings, "user_state": self.user_state}
)
return fragment
def studio_view(self, context):
fragment = Fragment()
context = {'fields': [], 'self': self}
# Build a list of all the fields that can be edited:
for field_name in self.editable_fields:
if field_name == "expected_result_positions":
continue
field = self.fields[field_name]
assert field.scope in (Scope.content, Scope.settings), (
"Only Scope.content or Scope.settings fields can be used with "
"StudioEditableXBlockMixin. Other scopes are for user-specific data and are "
"not generally created/configured by content authors in Studio."
)
field_info = self._make_field_info(field_name, field)
if field_info is not None:
context["fields"].append(field_info)
fragment.add_content(loader.render_template("templates/html/vectordraw_edit.html", context))
# Add resources to studio_view fragment
fragment.add_css_url(
self.runtime.local_resource_url(self, 'public/css/vectordraw.css')
)
fragment.add_css_url(
self.runtime.local_resource_url(self, 'public/css/vectordraw_edit.css')
)
fragment.add_javascript_url(
"//cdnjs.cloudflare.com/ajax/libs/jsxgraph/0.98/jsxgraphcore.js"
)
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/studio_edit.js')
)
fragment.add_javascript_url(
self.runtime.local_resource_url(self, 'public/js/vectordraw_edit.js')
)
fragment.initialize_js(
'VectorDrawXBlockEdit', {"settings": self.settings}
)
return fragment
def validate_field_data(self, validation, data):
"""
Validate this block's field data.
"""
super(VectorDrawXBlock, self).validate_field_data(validation, data)
def add_error(msg):
""" Helper function for adding validation messages. """
validation.add(ValidationMessage(ValidationMessage.ERROR, msg))
if data.background_url.strip():
if data.background_width == 0 and data.background_height == 0:
add_error(
u"You specified a background image but no width or height. "
"For the image to display, you need to specify a non-zero value "
"for at least one of them."
)
if not data.background_description.strip():
add_error(
u"No background description set. "
"This means that it will be more difficult for non-visual users "
"to solve the problem. "
"Please provide a description that contains sufficient information "
"that would allow anyone to solve the problem if the image did not load."
)
def _validate_check_answer_data(self, data): # pylint: disable=no-self-use
"""
Validate answer data submitted by user.
"""
# Check vectors
vectors = data.get('vectors')
if not isinstance(vectors, dict):
raise ValueError
for vector_data in vectors.values():
# Validate vector
if not vector_data.viewkeys() == {'tail', 'tip'}:
raise ValueError
# Validate tip and tail
tip = vector_data['tip']
tip_valid = isinstance(tip, list) and len(tip) == 2
tail = vector_data['tail']
tail_valid = isinstance(tail, list) and len(tail) == 2
if not (tip_valid and tail_valid):
raise ValueError
# Check points
points = data.get('points')
if not isinstance(points, dict):
raise ValueError
for coords in points.values():
# Validate point
point_valid = isinstance(coords, list) and len(coords) == 2
if not point_valid:
raise ValueError
# If we get to this point, it means that vector and point data is valid;
# the only thing left to check is whether data contains a list of checks:
if 'checks' not in data:
raise ValueError
@XBlock.json_handler
def check_answer(self, data, suffix=''): # pylint: disable=unused-argument
"""
Check and persist student's answer to this vector drawing problem.
"""
# Validate data
try:
self._validate_check_answer_data(data)
except ValueError:
raise JsonHandlerError(400, "Invalid data")
# Save answer
self.answer = dict(
vectors=data["vectors"],
points=data["points"]
)
# Compute result
grader = Grader()
result = grader.grade(data)
# Save result
self.result = result
# Publish grade data
score = 1 if result["correct"] else 0
self.runtime.publish(self, 'grade', dict(value=score, max_value=1))
return {
"result": result,
}
@staticmethod
def workbench_scenarios():
"""
Canned scenarios for display in the workbench.
"""
return loader.load_scenarios_from_path('templates/xml')
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