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.
This commit is contained in:
@@ -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"
|
||||
|
||||
20
common/lib/capa/capa/templates/formulaequationinput.html
Normal file
20
common/lib/capa/capa/templates/formulaequationinput.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<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.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- 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"> 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">
|
||||
|
||||
379
common/static/js/capa/spec/formula_equation_preview_spec.js
Normal file
379
common/static/js/capa/spec/formula_equation_preview_spec.js
Normal file
@@ -0,0 +1,379 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
describe("A jsinput has:", function () {
|
||||
xdescribe("A jsinput has:", function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$('#fixture').remove();
|
||||
161
common/static/js/capa/src/formula_equation_preview.js
Normal file
161
common/static/js/capa/src/formula_equation_preview.js
Normal file
@@ -0,0 +1,161 @@
|
||||
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|
|
||||
|
||||
Reference in New Issue
Block a user