Commit 735e3b01 by Peter Baratta

Create a new response type for Numerical/Formula

Named `FormulaEquationInput` (name up for debate)

- Based off ChemEqnIn
- Add FormulaEquationInput in inputtypes.py
- Add a call to a skeleton method for a preview

javascript:

- Queue up some MathJax
- Put some ordering on the AJAX requests: add a parameter when the request was started, when it returns check that it isn't outdated before displaying the preview
- Tests

Note: we moved the `jsinput` tests and DISABLED them, because they were causing the tests to fail.
parent a1162cbb
......@@ -16,6 +16,8 @@ Module containing the problem elements which render into input objects
- crystallography
- vsepr_input
- drag_and_drop
- formulaequationinput
- chemicalequationinput
These are matched by *.html files templates/*.html which are mako templates with the
actual html.
......@@ -47,6 +49,7 @@ import pyparsing
from .registry import TagRegistry
from chem import chemcalc
from preview import latex_preview
import xqueue_interface
from datetime import datetime
......@@ -531,7 +534,7 @@ class TextLine(InputTypeBase):
is used e.g. for embedding simulations turned into questions.
Example:
<texline math="1" trailing_text="m/s" />
<textline math="1" trailing_text="m/s" />
This example will render out a text line with a math preview and the text 'm/s'
after the end of the text line.
......@@ -1037,15 +1040,16 @@ class ChemicalEquationInput(InputTypeBase):
result = {'preview': '',
'error': ''}
formula = data['formula']
if formula is None:
try:
formula = data['formula']
except KeyError:
result['error'] = "No formula specified."
return result
try:
result['preview'] = chemcalc.render_to_html(formula)
except pyparsing.ParseException as p:
result['error'] = "Couldn't parse formula: {0}".format(p)
result['error'] = u"Couldn't parse formula: {0}".format(p.msg)
except Exception:
# this is unexpected, so log
log.warning(
......@@ -1056,6 +1060,98 @@ class ChemicalEquationInput(InputTypeBase):
registry.register(ChemicalEquationInput)
#-------------------------------------------------------------------------
class FormulaEquationInput(InputTypeBase):
"""
An input type for entering formula equations. Supports live preview.
Example:
<formulaequationinput size="50"/>
options: size -- width of the textbox.
"""
template = "formulaequationinput.html"
tags = ['formulaequationinput']
@classmethod
def get_attributes(cls):
"""
Can set size of text field.
"""
return [Attribute('size', '20'), ]
def _extra_context(self):
"""
TODO (vshnayder): Get rid of 'previewer' once we have a standard way of requiring js to be loaded.
"""
# `reported_status` is basically `status`, except we say 'unanswered'
reported_status = ''
if self.status == 'unsubmitted':
reported_status = 'unanswered'
elif self.status in ('correct', 'incorrect', 'incomplete'):
reported_status = self.status
return {
'previewer': '/static/js/capa/src/formula_equation_preview.js',
'reported_status': reported_status
}
def handle_ajax(self, dispatch, get):
'''
Since we only have formcalc preview this input, check to see if it
matches the corresponding dispatch and send it through if it does
'''
if dispatch == 'preview_formcalc':
return self.preview_formcalc(get)
return {}
def preview_formcalc(self, get):
"""
Render an preview of a formula or equation. `get` should
contain a key 'formula' with a math expression.
Returns a json dictionary:
{
'preview' : '<some latex>' or ''
'error' : 'the-error' or ''
'request_start' : <time sent with request>
}
"""
result = {'preview': '',
'error': ''}
try:
formula = get['formula']
except KeyError:
result['error'] = "No formula specified."
return result
result['request_start'] = int(get.get('request_start', 0))
try:
# TODO add references to valid variables and functions
# At some point, we might want to mark invalid variables as red
# or something, and this is where we would need to pass those in.
result['preview'] = latex_preview(formula)
except pyparsing.ParseException as err:
result['error'] = "Sorry, couldn't parse formula"
result['formula'] = formula
except Exception:
# this is unexpected, so log
log.warning(
"Error while previewing formula", exc_info=True
)
result['error'] = "Error while rendering preview"
return result
registry.register(FormulaEquationInput)
#-----------------------------------------------------------------------------
......
......@@ -822,7 +822,7 @@ class NumericalResponse(LoncapaResponse):
response_tag = 'numericalresponse'
hint_tag = 'numericalhint'
allowed_inputfields = ['textline']
allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer']
max_inputfields = 1
......@@ -837,11 +837,6 @@ class NumericalResponse(LoncapaResponse):
self.tolerance = contextualize_text(self.tolerance_xml, context)
except IndexError: # xpath found an empty list, so (...)[0] is the error
self.tolerance = '0'
try:
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0]
except IndexError: # Same as above
self.answer_id = None
def get_score(self, student_answers):
'''Grade a numeric response '''
......@@ -936,7 +931,7 @@ class CustomResponse(LoncapaResponse):
'chemicalequationinput', 'vsepr_input',
'drag_and_drop_input', 'editamoleculeinput',
'designprotein2dinput', 'editageneinput',
'annotationinput', 'jsinput']
'annotationinput', 'jsinput', 'formulaequationinput']
def setup_response(self):
xml = self.xml
......@@ -1692,7 +1687,7 @@ class FormulaResponse(LoncapaResponse):
response_tag = 'formularesponse'
hint_tag = 'formulahint'
allowed_inputfields = ['textline']
allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer', 'samples']
max_inputfields = 1
......@@ -1737,7 +1732,7 @@ class FormulaResponse(LoncapaResponse):
samples.split('@')[1].split('#')[0].split(':')))
ranges = dict(zip(variables, sranges))
for i in range(numsamples):
for _ in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict()
# ranges give numerical ranges for testing
......@@ -1767,27 +1762,39 @@ class FormulaResponse(LoncapaResponse):
)
except UndefinedVariable as uv:
log.debug(
'formularesponse: undefined variable in given=%s' % given)
'formularesponse: undefined variable in given=%s',
given
)
raise StudentInputError(
"Invalid input: " + uv.message + " not permitted in answer")
"Invalid input: " + uv.message + " not permitted in answer"
)
except ValueError as ve:
if 'factorial' in ve.message:
# This is thrown when fact() or factorial() is used in a formularesponse answer
# that tests on negative and/or non-integer inputs
# ve.message will be: `factorial() only accepts integral values` or `factorial() not defined for negative values`
# ve.message will be: `factorial() only accepts integral values` or
# `factorial() not defined for negative values`
log.debug(
'formularesponse: factorial function used in response that tests negative and/or non-integer inputs. given={0}'.format(given))
('formularesponse: factorial function used in response '
'that tests negative and/or non-integer inputs. '
'given={0}').format(given)
)
raise StudentInputError(
"factorial function not permitted in answer for this problem. Provided answer was: {0}".format(given))
("factorial function not permitted in answer "
"for this problem. Provided answer was: "
"{0}").format(cgi.escape(given))
)
# If non-factorial related ValueError thrown, handle it the same as any other Exception
log.debug('formularesponse: error {0} in formula'.format(ve))
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
except Exception as err:
# traceback.print_exc()
log.debug('formularesponse: error %s in formula' % err)
log.debug('formularesponse: error %s in formula', err)
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
cgi.escape(given))
# No errors in student's response--actually test for correctness
if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
return "incorrect"
return "correct"
......
<section id="formulaequationinput_${id}" class="formulaequationinput">
<div class="${reported_status}" id="status_${id}">
<input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">${reported_status}</p>
<div id="input_${id}_preview" class="equation">
\[\]
<img src="/static/images/spinner.gif" class="loading"/>
</div>
<p id="answer_${id}" class="answer"></p>
</div>
<div class="script_placeholder" data-src="${previewer}"/>
</section>
......@@ -448,6 +448,32 @@ class TextlineTemplateTest(TemplateTestCase):
self.assert_has_text(xml, xpath, self.context['msg'])
class FormulaEquationInputTemplateTest(TemplateTestCase):
"""
Test make template for `<formulaequationinput>`s.
"""
TEMPLATE_NAME = 'formulaequationinput.html'
def setUp(self):
self.context = {
'id': 2,
'value': 'PREFILLED_VALUE',
'status': 'unsubmitted',
'previewer': 'file.js',
'reported_status': 'REPORTED_STATUS',
}
super(FormulaEquationInputTemplateTest, self).setUp()
def test_no_size(self):
xml = self.render_to_xml(self.context)
self.assert_no_xpath(xml, "//input[@size]", self.context)
def test_size(self):
self.context['size'] = '40'
xml = self.render_to_xml(self.context)
self.assert_has_xpath(xml, "//input[@size='40']", self.context)
class AnnotationInputTemplateTest(TemplateTestCase):
"""
Test mako template for `<annotationinput>` input.
......
# -*- coding: utf-8 -*-
"""
Tests of input types.
......@@ -23,7 +24,8 @@ import xml.sax.saxutils as saxutils
from . import test_system
from capa import inputtypes
from mock import ANY
from mock import ANY, patch
from pyparsing import ParseException
# just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag
......@@ -47,7 +49,7 @@ class OptionInputTest(unittest.TestCase):
'status': 'answered'}
option_input = lookup_tag('optioninput')(test_system(), element, state)
context = option_input._get_render_context()
context = option_input._get_render_context() # pylint: disable=W0212
expected = {'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down')],
......@@ -94,7 +96,7 @@ class ChoiceGroupTest(unittest.TestCase):
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'sky_input',
'value': 'foil3',
......@@ -144,7 +146,7 @@ class JavascriptInputTest(unittest.TestCase):
state = {'value': '3', }
the_input = lookup_tag('javascriptinput')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'status': 'unanswered',
......@@ -172,7 +174,7 @@ class TextLineTest(unittest.TestCase):
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
......@@ -200,7 +202,7 @@ class TextLineTest(unittest.TestCase):
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
......@@ -238,7 +240,7 @@ class TextLineTest(unittest.TestCase):
state = {'value': 'BumbleBee', }
the_input = lookup_tag('textline')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'BumbleBee',
......@@ -276,7 +278,7 @@ class FileSubmissionTest(unittest.TestCase):
input_class = lookup_tag('filesubmission')
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'status': 'queued',
......@@ -321,7 +323,7 @@ class CodeInputTest(unittest.TestCase):
input_class = lookup_tag('codeinput')
the_input = input_class(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
......@@ -371,7 +373,7 @@ class MatlabTest(unittest.TestCase):
self.the_input = self.input_class(test_system(), elt, state)
def test_rendering(self):
context = self.the_input._get_render_context()
context = self.the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
......@@ -397,7 +399,7 @@ class MatlabTest(unittest.TestCase):
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
......@@ -424,7 +426,7 @@ class MatlabTest(unittest.TestCase):
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': status,
......@@ -449,7 +451,7 @@ class MatlabTest(unittest.TestCase):
elt = etree.fromstring(self.xml)
the_input = self.input_class(test_system(), elt, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'print "good evening"',
'status': 'queued',
......@@ -554,7 +556,7 @@ class SchematicTest(unittest.TestCase):
the_input = lookup_tag('schematic')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': value,
......@@ -593,7 +595,7 @@ class ImageInputTest(unittest.TestCase):
the_input = lookup_tag('imageinput')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': value,
......@@ -644,7 +646,7 @@ class CrystallographyTest(unittest.TestCase):
the_input = lookup_tag('crystallography')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': value,
......@@ -682,7 +684,7 @@ class VseprTest(unittest.TestCase):
the_input = lookup_tag('vsepr_input')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': value,
......@@ -711,7 +713,7 @@ class ChemicalEquationTest(unittest.TestCase):
def test_rendering(self):
''' Verify that the render context matches the expected render context'''
context = self.the_input._get_render_context()
context = self.the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': 'H2OYeah',
......@@ -727,10 +729,168 @@ class ChemicalEquationTest(unittest.TestCase):
data = {'formula': "H"}
response = self.the_input.handle_ajax("preview_chemcalc", data)
self.assertTrue('preview' in response)
self.assertIn('preview', response)
self.assertNotEqual(response['preview'], '')
self.assertEqual(response['error'], "")
def test_ajax_bad_method(self):
"""
With a bad dispatch, we shouldn't recieve anything
"""
response = self.the_input.handle_ajax("obviously_not_real", {})
self.assertEqual(response, {})
def test_ajax_no_formula(self):
"""
When we ask for a formula rendering, there should be an error if no formula
"""
response = self.the_input.handle_ajax("preview_chemcalc", {})
self.assertIn('error', response)
self.assertEqual(response['error'], "No formula specified.")
def test_ajax_parse_err(self):
"""
With parse errors, ChemicalEquationInput should give an error message
"""
# Simulate answering a problem that raises the exception
with patch('capa.inputtypes.chemcalc.render_to_html') as mock_render:
mock_render.side_effect = ParseException(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ")
response = self.the_input.handle_ajax(
"preview_chemcalc",
{'formula': 'H2O + invalid chemistry'}
)
self.assertIn('error', response)
self.assertTrue("Couldn't parse formula" in response['error'])
@patch('capa.inputtypes.log')
def test_ajax_other_err(self, mock_log):
"""
With other errors, test that ChemicalEquationInput also logs it
"""
with patch('capa.inputtypes.chemcalc.render_to_html') as mock_render:
mock_render.side_effect = Exception()
response = self.the_input.handle_ajax(
"preview_chemcalc",
{'formula': 'H2O + superterrible chemistry'}
)
mock_log.warning.assert_called_once_with(
"Error while previewing chemical formula", exc_info=True
)
self.assertIn('error', response)
self.assertEqual(response['error'], "Error while rendering preview")
class FormulaEquationTest(unittest.TestCase):
"""
Check that formula equation inputs work.
"""
def setUp(self):
self.size = "42"
xml_str = """<formulaequationinput id="prob_1_2" size="{size}"/>""".format(size=self.size)
element = etree.fromstring(xml_str)
state = {'value': 'x^2+1/2'}
self.the_input = lookup_tag('formulaequationinput')(test_system(), element, state)
def test_rendering(self):
"""
Verify that the render context matches the expected render context
"""
context = self.the_input._get_render_context() # pylint: disable=W0212
expected = {
'id': 'prob_1_2',
'value': 'x^2+1/2',
'status': 'unanswered',
'reported_status': '',
'msg': '',
'size': self.size,
'previewer': '/static/js/capa/src/formula_equation_preview.js',
}
self.assertEqual(context, expected)
def test_rendering_reported_status(self):
"""
Verify that the 'reported status' matches expectations.
"""
test_values = {
'': '', # Default
'unsubmitted': 'unanswered',
'correct': 'correct',
'incorrect': 'incorrect',
'incomplete': 'incomplete',
'not a status': ''
}
for self_status, reported_status in test_values.iteritems():
self.the_input.status = self_status
context = self.the_input._get_render_context() # pylint: disable=W0212
self.assertEqual(context['reported_status'], reported_status)
def test_formcalc_ajax_sucess(self):
"""
Verify that using the correct dispatch and valid data produces a valid response
"""
data = {'formula': "x^2+1/2", 'request_start': 0}
response = self.the_input.handle_ajax("preview_formcalc", data)
self.assertIn('preview', response)
self.assertNotEqual(response['preview'], '')
self.assertEqual(response['error'], "")
self.assertEqual(response['request_start'], data['request_start'])
def test_ajax_bad_method(self):
"""
With a bad dispatch, we shouldn't recieve anything
"""
response = self.the_input.handle_ajax("obviously_not_real", {})
self.assertEqual(response, {})
def test_ajax_no_formula(self):
"""
When we ask for a formula rendering, there should be an error if no formula
"""
response = self.the_input.handle_ajax(
"preview_formcalc",
{'request_start': 1, }
)
self.assertIn('error', response)
self.assertEqual(response['error'], "No formula specified.")
def test_ajax_parse_err(self):
"""
With parse errors, FormulaEquationInput should give an error message
"""
# Simulate answering a problem that raises the exception
with patch('capa.inputtypes.latex_preview') as mock_preview:
mock_preview.side_effect = ParseException("Oopsie")
response = self.the_input.handle_ajax(
"preview_formcalc",
{'formula': 'x^2+1/2', 'request_start': 1, }
)
self.assertIn('error', response)
self.assertEqual(response['error'], "Sorry, couldn't parse formula")
@patch('capa.inputtypes.log')
def test_ajax_other_err(self, mock_log):
"""
With other errors, test that FormulaEquationInput also logs it
"""
with patch('capa.inputtypes.latex_preview') as mock_preview:
mock_preview.side_effect = Exception()
response = self.the_input.handle_ajax(
"preview_formcalc",
{'formula': 'x^2+1/2', 'request_start': 1, }
)
mock_log.warning.assert_called_once_with(
"Error while previewing formula", exc_info=True
)
self.assertIn('error', response)
self.assertEqual(response['error'], "Error while rendering preview")
class DragAndDropTest(unittest.TestCase):
'''
......@@ -784,7 +944,7 @@ class DragAndDropTest(unittest.TestCase):
the_input = lookup_tag('drag_and_drop_input')(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {'id': 'prob_1_2',
'value': value,
'status': 'unsubmitted',
......@@ -833,7 +993,7 @@ class AnnotationInputTest(unittest.TestCase):
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
expected = {
'id': 'annotation_input',
......@@ -920,7 +1080,7 @@ class TestChoiceText(unittest.TestCase):
}
expected.update(state)
the_input = lookup_tag(tag)(test_system(), element, state)
context = the_input._get_render_context()
context = the_input._get_render_context() # pylint: disable=W0212
self.assertEqual(context, expected)
def test_radiotextgroup(self):
......
......@@ -32,7 +32,7 @@ $wrongans</tt> to see a hint.</p>
<formularesponse samples="x@-5:5#11" id="11" answer="$answer">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.001" name="tol" />
<text>y = <textline size="25" /></text>
<text>y = <formulaequationinput size="25" /></text>
<hintgroup>
<formulahint samples="x@-5:5#11" answer="$wrongans" name="inversegrad">
</formulahint>
......
......@@ -173,7 +173,7 @@ section.problem {
}
}
&.incorrect, &.ui-icon-close {
&.incorrect, &.incomplete, &.ui-icon-close {
p.status {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
......@@ -214,6 +214,16 @@ section.problem {
clear: both;
margin-top: 3px;
.MathJax_Display {
display: inline-block;
width: auto;
}
img.loading {
display: inline-block;
padding-left: 10px;
}
span {
margin-bottom: 0;
......@@ -265,7 +275,7 @@ section.problem {
width: 25px;
}
&.incorrect, &.ui-icon-close {
&.incorrect, &.incomplete, &.ui-icon-close {
@include inline-block();
background: url('../images/incorrect-icon.png') center center no-repeat;
height: 20px;
......
......@@ -121,18 +121,18 @@ describe 'MarkdownEditingDescriptor', ->
<p>Enter the numerical value of Pi:</p>
<numericalresponse answer="3.14159">
<responseparam type="tolerance" default=".02" />
<textline />
<formulaequationinput />
</numericalresponse>
<p>Enter the approximate value of 502*9:</p>
<numericalresponse answer="4518">
<responseparam type="tolerance" default="15%" />
<textline />
<formulaequationinput />
</numericalresponse>
<p>Enter the number of fingers on a human hand:</p>
<numericalresponse answer="5">
<textline />
<formulaequationinput />
</numericalresponse>
<solution>
......@@ -157,7 +157,7 @@ describe 'MarkdownEditingDescriptor', ->
<p>Enter 0 with a tolerance:</p>
<numericalresponse answer="0">
<responseparam type="tolerance" default=".02" />
<textline />
<formulaequationinput />
</numericalresponse>
......
......@@ -239,7 +239,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
} else {
string = '<numericalresponse answer="' + floatValue + '">\n';
}
string += ' <textline />\n';
string += ' <formulaequationinput />\n';
string += '</numericalresponse>\n\n';
} else {
string = '<stringresponse answer="' + p + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
......
......@@ -24,15 +24,15 @@ data: |
</script>
<p>Give an equation for the relativistic energy of an object with mass m. Explicitly indicate multiplication with a <tt>*</tt> symbol.</p>
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="m*c^2">
<responseparam type="tolerance" default="0.00001"/>
<br/><text>E =</text> <textline size="40" math="1" />
<responseparam type="tolerance" default="0.00001"/>
<br/><text>E =</text> <formulaequationinput size="40" />
</formularesponse>
<p>The answer to this question is (R_1*R_2)/R_3. </p>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="$VoVi">
<responseparam type="tolerance" default="0.00001"/>
<textline size="40" math="1" />
<formulaequationinput size="40" />
</formularesponse>
<solution>
<div class="detailed-solution">
......
......@@ -119,9 +119,8 @@ data: |
<p>
<p style="display:inline">Energy saved = </p>
<numericalresponse inline="1" answer="0.52">
<textline inline="1">
<responseparam description="Numerical Tolerance" type="tolerance" default="0.02" name="tol"/>
</textline>
<responseparam description="Numerical Tolerance" type="tolerance" default="0.02" name="tol"/>
<formulaequationinput/>
</numericalresponse>
<p style="display:inline">&#xA0;EJ/year</p>
</p>
......
......@@ -47,19 +47,19 @@ data: |
<p>Enter the numerical value of Pi:
<numericalresponse answer="3.14159">
<responseparam type="tolerance" default=".02" />
<textline />
<formulaequationinput />
</numericalresponse>
</p>
<p>Enter the approximate value of 502*9:
<numericalresponse answer="$computed_response">
<responseparam type="tolerance" default="15%"/>
<textline />
<formulaequationinput />
</numericalresponse>
</p>
<p>Enter the number of fingers on a human hand:
<numericalresponse answer="5">
<textline />
<formulaequationinput />
</numericalresponse>
</p>
<solution>
......
......@@ -28,7 +28,7 @@ class CHModuleFactory(object):
<p>The answer is correct if it is within a specified numerical tolerance of the expected answer.</p>
<p>Enter the number of fingers on a human hand:</p>
<numericalresponse answer="5">
<textline/>
<formulaequationinput/>
</numericalresponse>
<solution>
<div class="detailed-solution">
......@@ -114,7 +114,7 @@ class VerticalWithModulesFactory(object):
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Test numerical problem.</p>
<numericalresponse answer="5">
<textline/>
<formulaequationinput/>
</numericalresponse>
<solution>
<div class="detailed-solution">
......@@ -129,7 +129,7 @@ class VerticalWithModulesFactory(object):
<problem display_name="Numerical Input" markdown=" " rerandomize="never" showanswer="finished">
<p>Another test numerical problem.</p>
<numericalresponse answer="5">
<textline/>
<formulaequationinput/>
</numericalresponse>
<solution>
<div class="detailed-solution">
......
function callPeriodicallyUntil(block, delay, condition, i) { // i is optional
i = i || 0;
block(i);
waits(delay);
runs(function () {
if (!condition()) {
callPeriodicallyUntil(block, delay, condition, i + 1);
}
});
}
describe("Formula Equation Preview", function () {
beforeEach(function () {
// Simulate an environment conducive to a FormulaEquationInput
var $fixture = this.$fixture = $('\
<section class="problems-wrapper" data-url="THE_URL">\
<section class="formulaequationinput">\
<input type="text" id="input_THE_ID" data-input-id="THE_ID"\
value="prefilled_value"/>\
<div id="input_THE_ID_preview" class="equation">\
\[\]\
<img class="loading" style="visibility:hidden"/>\
</div>\
</section>\
</section>');
// Modify $ for the test to search the fixture.
var old$find = this.old$find = $.find;
$.find = function () {
// Given the default context, swap it out for the fixture.
if (arguments[1] == document) {
arguments[1] = $fixture[0];
}
// Call old function.
return old$find.apply(this, arguments);
}
$.find.matchesSelector = old$find.matchesSelector;
this.oldDGEBI = document.getElementById;
document.getElementById = function (id) {
return $("*#" + id)[0] || null;
};
// Catch the AJAX requests
var ajaxTimes = this.ajaxTimes = [];
this.oldProblem = window.Problem;
window.Problem = {};
Problem.inputAjax = jasmine.createSpy('Problem.inputAjax')
.andCallFake(function () {
ajaxTimes.push(Date.now());
});
// Spy on MathJax
this.jax = 'OUTPUT_JAX';
this.oldMathJax = window.MathJax;
window.MathJax = {Hub: {}};
MathJax.Hub.getAllJax = jasmine.createSpy('MathJax.Hub.getAllJax')
.andReturn([this.jax]);
MathJax.Hub.Queue = jasmine.createSpy('MathJax.Hub.Queue');
});
it('(the test) should be able to swap out the behavior of $', function () {
// This was a pain to write, make sure it doesn't get screwed up.
// Find the DOM element using DOM methods.
var legitInput = this.$fixture[0].getElementsByTagName("input")[0];
// Use the (modified) jQuery.
var jqueryInput = $('.formulaequationinput input');
var byIdInput = $("#input_THE_ID");
expect(jqueryInput[0]).toEqual(legitInput);
expect(byIdInput[0]).toEqual(legitInput);
});
describe('Ajax requests', function () {
it('has an initial request with the correct parameters', function () {
formulaEquationPreview.enable();
expect(MathJax.Hub.Queue).toHaveBeenCalled();
// Do what Queue would've done--call the function.
var args = MathJax.Hub.Queue.mostRecentCall.args;
args[1].call(args[0]);
// This part may be asynchronous, so wait.
waitsFor(function () {
return Problem.inputAjax.wasCalled;
}, "AJAX never called initially", 1000);
runs(function () {
expect(Problem.inputAjax.callCount).toEqual(1);
// Use `.toEqual` rather than `.toHaveBeenCalledWith`
// since it supports `jasmine.any`.
expect(Problem.inputAjax.mostRecentCall.args).toEqual([
"THE_URL",
"THE_ID",
"preview_formcalc",
{formula: "prefilled_value",
request_start: jasmine.any(Number)},
jasmine.any(Function)
]);
});
});
it('makes a request on user input', function () {
formulaEquationPreview.enable();
$('#input_THE_ID').val('user_input').trigger('input');
// This part is probably asynchronous
waitsFor(function () {
return Problem.inputAjax.wasCalled;
}, "AJAX never called on user input", 1000);
runs(function () {
expect(Problem.inputAjax.mostRecentCall.args[3].formula
).toEqual('user_input');
});
});
it("shouldn't be requested for empty input", function () {
formulaEquationPreview.enable();
MathJax.Hub.Queue.reset();
// When we make an input of '',
$('#input_THE_ID').val('').trigger('input');
// Either it makes a request or jumps straight into displaying ''.
waitsFor(function () {
// (Short circuit if `inputAjax` is indeed called)
return Problem.inputAjax.wasCalled ||
MathJax.Hub.Queue.wasCalled;
}, "AJAX never called on user input", 1000);
runs(function () {
// Expect the request not to have been called.
expect(Problem.inputAjax).not.toHaveBeenCalled();
});
});
it('should limit the number of requests per second', function () {
formulaEquationPreview.enable();
var minDelay = formulaEquationPreview.minDelay;
var end = Date.now() + minDelay * 1.1;
var step = 10; // ms
var $input = $('#input_THE_ID');
var value;
function inputAnother(iter) {
value = "math input " + iter;
$input.val(value).trigger('input');
}
callPeriodicallyUntil(inputAnother, step, function () {
return Date.now() > end; // Stop when we get to `end`.
});
waitsFor(function () {
return Problem.inputAjax.wasCalled &&
Problem.inputAjax.mostRecentCall.args[3].formula == value;
}, "AJAX never called with final value from input", 1000);
runs(function () {
// There should be 2 or 3 calls (depending on leading edge).
expect(Problem.inputAjax.callCount).not.toBeGreaterThan(3);
// The calls should happen approximately `minDelay` apart.
for (var i =1; i < this.ajaxTimes.length; i ++) {
var diff = this.ajaxTimes[i] - this.ajaxTimes[i - 1];
expect(diff).toBeGreaterThan(minDelay - 10);
}
});
});
});
describe("Visible results (icon and mathjax)", function () {
it('should display a loading icon when requests are open', function () {
formulaEquationPreview.enable();
var $img = $("img.loading");
expect($img.css('visibility')).toEqual('hidden');
$("#input_THE_ID").val("different").trigger('input');
expect($img.css('visibility')).toEqual('visible');
// Don't let it fail later.
waitsFor(function () {
return Problem.inputAjax.wasCalled;
});
});
it('should update MathJax and loading icon on callback', function () {
formulaEquationPreview.enable();
$('#input_THE_ID').val('user_input').trigger('input');
waitsFor(function () {
return Problem.inputAjax.wasCalled;
}, "AJAX never called initially", 1000);
runs(function () {
var args = Problem.inputAjax.mostRecentCall.args;
var callback = args[4];
callback({
preview: 'THE_FORMULA',
request_start: args[3].request_start
});
// The only request returned--it should hide the loading icon.
expect($("img.loading").css('visibility')).toEqual('hidden');
// We should look in the preview div for the MathJax.
var previewDiv = $("div")[0];
expect(MathJax.Hub.getAllJax).toHaveBeenCalledWith(previewDiv);
// Refresh the MathJax.
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, 'THE_FORMULA'],
['Reprocess', this.jax]
);
});
});
it('should display errors from the server well', function () {
var $img = $("img.loading");
formulaEquationPreview.enable();
MathJax.Hub.Queue.reset();
$("#input_THE_ID").val("different").trigger('input');
waitsFor(function () {
return Problem.inputAjax.wasCalled;
}, "AJAX never called initially", 1000);
runs(function () {
var args = Problem.inputAjax.mostRecentCall.args;
var callback = args[4];
callback({
error: 'OOPSIE',
request_start: args[3].request_start
});
expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
expect($img.css('visibility')).toEqual('visible');
});
var errorDelay = formulaEquationPreview.errorDelay * 1.1;
waitsFor(function () {
return MathJax.Hub.Queue.wasCalled;
}, "Error message never displayed", 2000);
runs(function () {
// Refresh the MathJax.
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, '\\text{OOPSIE}'],
['Reprocess', this.jax]
);
expect($img.css('visibility')).toEqual('hidden');
});
});
});
describe('Multiple callbacks', function () {
beforeEach(function () {
formulaEquationPreview.enable();
MathJax.Hub.Queue.reset();
$('#input_THE_ID').val('different').trigger('input');
waitsFor(function () {
return Problem.inputAjax.wasCalled;
});
runs(function () {
$("#input_THE_ID").val("different2").trigger('input');
});
waitsFor(function () {
return Problem.inputAjax.callCount > 1;
});
runs(function () {
var args = Problem.inputAjax.argsForCall;
var response0 = {
preview: 'THE_FORMULA_0',
request_start: args[0][3].request_start
};
var response1 = {
preview: 'THE_FORMULA_1',
request_start: args[1][3].request_start
};
this.callbacks = [args[0][4], args[1][4]];
this.responses = [response0, response1];
});
});
it('should update requests sequentially', function () {
var $img = $("img.loading");
expect($img.css('visibility')).toEqual('visible');
this.callbacks[0](this.responses[0]);
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, 'THE_FORMULA_0'],
['Reprocess', this.jax]
);
expect($img.css('visibility')).toEqual('visible');
this.callbacks[1](this.responses[1]);
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, 'THE_FORMULA_1'],
['Reprocess', this.jax]
);
expect($img.css('visibility')).toEqual('hidden')
});
it("shouldn't display outdated information", function () {
var $img = $("img.loading");
expect($img.css('visibility')).toEqual('visible');
// Switch the order (1 returns before 0)
this.callbacks[1](this.responses[1]);
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, 'THE_FORMULA_1'],
['Reprocess', this.jax]
);
expect($img.css('visibility')).toEqual('hidden')
MathJax.Hub.Queue.reset();
this.callbacks[0](this.responses[0]);
expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
expect($img.css('visibility')).toEqual('hidden')
});
it("shouldn't show an error if the responses are close together",
function () {
this.callbacks[0]({
error: 'OOPSIE',
request_start: this.responses[0].request_start
});
expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
// Error message waiting to be displayed
this.callbacks[1](this.responses[1]);
expect(MathJax.Hub.Queue).toHaveBeenCalledWith(
['Text', this.jax, 'THE_FORMULA_1'],
['Reprocess', this.jax]
);
// Make sure that it doesn't indeed show up later
MathJax.Hub.Queue.reset();
var errorDelay = formulaEquationPreview.errorDelay * 1.1;
waits(errorDelay);
runs(function () {
expect(MathJax.Hub.Queue).not.toHaveBeenCalled();
})
});
});
afterEach(function () {
// Return jQuery
$.find = this.old$find;
document.getElementById = this.oldDGEBI;
// Return Problem
Problem = this.oldProblem;
if (Problem === undefined) {
delete Problem;
}
// Return MathJax
MathJax = this.oldMathJax;
if (MathJax === undefined) {
delete MathJax;
}
});
});
describe("A jsinput has:", function () {
xdescribe("A jsinput has:", function () {
beforeEach(function () {
$('#fixture').remove();
......
var formulaEquationPreview = {
minDelay: 300, // Minimum time between requests sent out.
errorDelay: 1500 // Wait time before showing error (prevent frustration).
};
/** Setup the FormulaEquationInputs and associated javascript code. */
formulaEquationPreview.enable = function () {
/**
* Accumulate all the variables and attach event handlers.
* This includes rate-limiting `sendRequest` and creating a closure for
* its callback.
*/
function setupInput() {
var $this = $(this); // cache the jQuery object
var $preview = $("#" + this.id + "_preview");
var inputData = {
// These are the mutable values
lastSent: 0,
isWaitingForRequest: false,
requestVisible: 0,
errorDelayTimeout: null,
// The following don't change
// Find the URL from the closest parent problems-wrapper.
url: $this.closest('.problems-wrapper').data('url'),
// Grab the input id from the input.
inputId: $this.data('input-id'),
// Store the DOM/MathJax elements in which visible output occurs.
$preview: $preview,
// Note: sometimes MathJax hasn't finished loading yet.
jax: MathJax.Hub.getAllJax($preview[0])[0],
$img: $preview.find("img.loading"),
requestCallback: null // Fill it in in a bit.
};
// Give callback access to `inputData` (fill in first parameter).
inputData.requestCallback = _.partial(updatePage, inputData);
// Limit `sendRequest` and have it show the loading icon.
var throttledRequest = _.throttle(
sendRequest,
formulaEquationPreview.minDelay,
{leading: false}
);
// The following acts as a closure of `inputData`.
var initializeRequest = function () {
// Show the loading icon.
inputData.$img.css('visibility', 'visible');
inputData.isWaitingForRequest = true;
throttledRequest(inputData, this.value);
};
$this.on("input", initializeRequest);
// send an initial
MathJax.Hub.Queue(this, initializeRequest);
}
/**
* Fire off a request for a preview of the current value.
* Also send along the time it was sent, and store that locally.
*/
function sendRequest(inputData, formula) {
// Save the time.
var now = Date.now();
inputData.lastSent = now;
// We're sending it.
inputData.isWaitingForRequest = false;
if (formula) {
// Send the request.
Problem.inputAjax(
inputData.url,
inputData.inputId,
'preview_formcalc',
{"formula" : formula, "request_start" : now},
inputData.requestCallback
);
// ).fail(function () {
// // This is run when ajax call fails.
// // Have an error message and other stuff here?
// inputData.$img.css('visibility', 'hidden');
// }); */
}
else {
inputData.requestCallback({
preview: '',
request_start: now
});
}
}
/**
* Respond to the preview request if need be.
* Stop if it is outdated (i.e. a later request arrived back earlier)
* Otherwise:
* -Refresh the MathJax
* -Stop the loading icon if this is the most recent request
* -Save which request is visible
*/
function updatePage(inputData, response) {
var requestStart = response['request_start'];
if (requestStart == inputData.lastSent &&
!inputData.isWaitingForRequest) {
// Disable icon.
inputData.$img.css('visibility', 'hidden');
}
if (requestStart <= inputData.requestVisible) {
// This is an old request.
return;
}
// Save the value of the last response displayed.
inputData.requestVisible = requestStart;
// Prevent an old error message from showing.
if (inputData.errorWaitTimeout != null) {
window.clearTimeout(inputData.errorWaitTimeout);
}
function display(latex) {
// Load jax if it failed before.
if (!inputData.jax) {
results = MathJax.Hub.getAllJax(inputData.$preview[0]);
if (!results.length) {
console.log("Unable to find MathJax to display");
return;
}
inputData.jax = results[0];
}
// Set the text as the latex code, and then update the MathJax.
MathJax.Hub.Queue(
['Text', inputData.jax, latex],
['Reprocess', inputData.jax]
);
}
if (response.error) {
inputData.$img.css('visibility', 'visible');
inputData.errorWaitTimeout = window.setTimeout(function () {
display("\\text{" + response.error + "}");
inputData.$img.css('visibility', 'hidden');
}, formulaEquationPreview.errorDelay);
} else {
display(response.preview);
}
}
// Invoke the setup method.
$('.formulaequationinput input').each(setupInput);
};
formulaEquationPreview.enable();
......@@ -121,6 +121,7 @@ end
static_js_dirs = Dir["common/lib/*"].select{|lib| File.directory?(lib)}
static_js_dirs << 'common/static/coffee'
static_js_dirs << 'common/static/js'
static_js_dirs.select!{|lib| !Dir["#{lib}/**/spec"].empty?}
static_js_dirs.each do |dir|
......
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