Commit bfccfba9 by Peter Baratta

Merge pull request #512 from edx/peterb/formula-preview

Calc module changes: previewing and <formulaequationinput>

To see individual commits, see tag peterb/formula-preview/presquash
parents d1e0005f 69fa1b06
# -*- coding: utf-8 -*-
"""
Unit tests for preview.py
"""
import unittest
import preview
import pyparsing
class LatexRenderedTest(unittest.TestCase):
"""
Test the initializing code for LatexRendered.
Specifically that it stores the correct data and handles parens well.
"""
def test_simple(self):
"""
Test that the data values are stored without changing.
"""
math = 'x^2'
obj = preview.LatexRendered(math, tall=True)
self.assertEquals(obj.latex, math)
self.assertEquals(obj.sans_parens, math)
self.assertEquals(obj.tall, True)
def _each_parens(self, with_parens, math, parens, tall=False):
"""
Helper method to test the way parens are wrapped.
"""
obj = preview.LatexRendered(math, parens=parens, tall=tall)
self.assertEquals(obj.latex, with_parens)
self.assertEquals(obj.sans_parens, math)
self.assertEquals(obj.tall, tall)
def test_parens(self):
""" Test curvy parens. """
self._each_parens('(x+y)', 'x+y', '(')
def test_brackets(self):
""" Test brackets. """
self._each_parens('[x+y]', 'x+y', '[')
def test_squiggles(self):
""" Test curly braces. """
self._each_parens(r'\{x+y\}', 'x+y', '{')
def test_parens_tall(self):
""" Test curvy parens with the tall parameter. """
self._each_parens(r'\left(x^y\right)', 'x^y', '(', tall=True)
def test_brackets_tall(self):
""" Test brackets, also tall. """
self._each_parens(r'\left[x^y\right]', 'x^y', '[', tall=True)
def test_squiggles_tall(self):
""" Test tall curly braces. """
self._each_parens(r'\left\{x^y\right\}', 'x^y', '{', tall=True)
def test_bad_parens(self):
""" Check that we get an error with invalid parens. """
with self.assertRaisesRegexp(Exception, 'Unknown parenthesis'):
preview.LatexRendered('x^2', parens='not parens')
class LatexPreviewTest(unittest.TestCase):
"""
Run integrative tests for `latex_preview`.
All functionality was tested `RenderMethodsTest`, but see if it combines
all together correctly.
"""
def test_no_input(self):
"""
With no input (including just whitespace), see that no error is thrown.
"""
self.assertEquals('', preview.latex_preview(''))
self.assertEquals('', preview.latex_preview(' '))
self.assertEquals('', preview.latex_preview(' \t '))
def test_number_simple(self):
""" Simple numbers should pass through. """
self.assertEquals(preview.latex_preview('3.1415'), '3.1415')
def test_number_suffix(self):
""" Suffixes should be escaped. """
self.assertEquals(preview.latex_preview('1.618k'), r'1.618\text{k}')
def test_number_sci_notation(self):
""" Numbers with scientific notation should display nicely """
self.assertEquals(
preview.latex_preview('6.0221413E+23'),
r'6.0221413\!\times\!10^{+23}'
)
self.assertEquals(
preview.latex_preview('-6.0221413E+23'),
r'-6.0221413\!\times\!10^{+23}'
)
def test_number_sci_notation_suffix(self):
""" Test numbers with both of these. """
self.assertEquals(
preview.latex_preview('6.0221413E+23k'),
r'6.0221413\!\times\!10^{+23}\text{k}'
)
self.assertEquals(
preview.latex_preview('-6.0221413E+23k'),
r'-6.0221413\!\times\!10^{+23}\text{k}'
)
def test_variable_simple(self):
""" Simple valid variables should pass through. """
self.assertEquals(preview.latex_preview('x', variables=['x']), 'x')
def test_greek(self):
""" Variable names that are greek should be formatted accordingly. """
self.assertEquals(preview.latex_preview('pi'), r'\pi')
def test_variable_subscript(self):
""" Things like 'epsilon_max' should display nicely """
self.assertEquals(
preview.latex_preview('epsilon_max', variables=['epsilon_max']),
r'\epsilon_{max}'
)
def test_function_simple(self):
""" Valid function names should be escaped. """
self.assertEquals(
preview.latex_preview('f(3)', functions=['f']),
r'\text{f}(3)'
)
def test_function_tall(self):
r""" Functions surrounding a tall element should have \left, \right """
self.assertEquals(
preview.latex_preview('f(3^2)', functions=['f']),
r'\text{f}\left(3^{2}\right)'
)
def test_function_sqrt(self):
""" Sqrt function should be handled specially. """
self.assertEquals(preview.latex_preview('sqrt(3)'), r'\sqrt{3}')
def test_function_log10(self):
""" log10 function should be handled specially. """
self.assertEquals(preview.latex_preview('log10(3)'), r'\log_{10}(3)')
def test_function_log2(self):
""" log2 function should be handled specially. """
self.assertEquals(preview.latex_preview('log2(3)'), r'\log_2(3)')
def test_power_simple(self):
""" Powers should wrap the elements with braces correctly. """
self.assertEquals(preview.latex_preview('2^3^4'), '2^{3^{4}}')
def test_power_parens(self):
""" Powers should ignore the parenthesis of the last math. """
self.assertEquals(preview.latex_preview('2^3^(4+5)'), '2^{3^{4+5}}')
def test_parallel(self):
r""" Parallel items should combine with '\|'. """
self.assertEquals(preview.latex_preview('2||3'), r'2\|3')
def test_product_mult_only(self):
r""" Simple products should combine with a '\cdot'. """
self.assertEquals(preview.latex_preview('2*3'), r'2\cdot 3')
def test_product_big_frac(self):
""" Division should combine with '\frac'. """
self.assertEquals(
preview.latex_preview('2*3/4/5'),
r'\frac{2\cdot 3}{4\cdot 5}'
)
def test_product_single_frac(self):
""" Division should ignore parens if they are extraneous. """
self.assertEquals(
preview.latex_preview('(2+3)/(4+5)'),
r'\frac{2+3}{4+5}'
)
def test_product_keep_going(self):
"""
Complex products/quotients should split into many '\frac's when needed.
"""
self.assertEquals(
preview.latex_preview('2/3*4/5*6'),
r'\frac{2}{3}\cdot \frac{4}{5}\cdot 6'
)
def test_sum(self):
""" Sums should combine its elements. """
# Use 'x' as the first term (instead of, say, '1'), so it can't be
# interpreted as a negative number.
self.assertEquals(
preview.latex_preview('-x+2-3+4', variables=['x']),
'-x+2-3+4'
)
def test_sum_tall(self):
""" A complicated expression should not hide the tallness. """
self.assertEquals(
preview.latex_preview('(2+3^2)'),
r'\left(2+3^{2}\right)'
)
def test_complicated(self):
"""
Given complicated input, ensure that exactly the correct string is made.
"""
self.assertEquals(
preview.latex_preview('11*f(x)+x^2*(3||4)/sqrt(pi)'),
r'11\cdot \text{f}(x)+\frac{x^{2}\cdot (3\|4)}{\sqrt{\pi}}'
)
self.assertEquals(
preview.latex_preview('log10(1+3/4/Cos(x^2)*(x+1))',
case_sensitive=True),
(r'\log_{10}\left(1+\frac{3}{4\cdot \text{Cos}\left(x^{2}\right)}'
r'\cdot (x+1)\right)')
)
def test_syntax_errors(self):
"""
Test a lot of math strings that give syntax errors
Rather than have a lot of self.assertRaises, make a loop and keep track
of those that do not throw a `ParseException`, and assert at the end.
"""
bad_math_list = [
'11+',
'11*',
'f((x)',
'sqrt(x^)',
'3f(x)', # Not 3*f(x)
'3|4',
'3|||4'
]
bad_exceptions = {}
for math in bad_math_list:
try:
preview.latex_preview(math)
except pyparsing.ParseException:
pass # This is what we were expecting. (not excepting :P)
except Exception as error: # pragma: no cover
bad_exceptions[math] = error
else: # pragma: no cover
# If there is no exception thrown, this is a problem
bad_exceptions[math] = None
self.assertEquals({}, bad_exceptions)
......@@ -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
......@@ -1748,38 +1743,58 @@ class FormulaResponse(LoncapaResponse):
student_variables[str(var)] = value
# log.debug('formula: instructor_vars=%s, expected=%s' %
# (instructor_variables,expected))
instructor_result = evaluator(instructor_variables, dict(),
expected, cs=self.case_sensitive)
# Call `evaluator` on the instructor's answer and get a number
instructor_result = evaluator(
instructor_variables, dict(),
expected, case_sensitive=self.case_sensitive
)
try:
# log.debug('formula: student_vars=%s, given=%s' %
# (student_variables,given))
student_result = evaluator(student_variables,
dict(),
given,
cs=self.case_sensitive)
# Call `evaluator` on the student's answer; look for exceptions
student_result = evaluator(
student_variables,
dict(),
given,
case_sensitive=self.case_sensitive
)
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.
......
......@@ -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>
......
......@@ -71,51 +71,6 @@ class ModelsTest(unittest.TestCase):
vc_str = "<class 'xmodule.video_module.VideoDescriptor'>"
self.assertEqual(str(vc), vc_str)
def test_calc(self):
variables = {'R1': 2.0, 'R3': 4.0}
functions = {'sin': numpy.sin, 'cos': numpy.cos}
self.assertTrue(abs(calc.evaluator(variables, functions, "10000||sin(7+5)+0.5356")) < 0.01)
self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13)
self.assertEqual(calc.evaluator(variables, functions, "13"), 13)
self.assertEqual(calc.evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5)
self.assertEqual(calc.evaluator({}, {}, "-1"), -1)
self.assertEqual(calc.evaluator({}, {}, "-0.33"), -.33)
self.assertEqual(calc.evaluator({}, {}, "-.33"), -.33)
self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0)
self.assertTrue(abs(calc.evaluator(variables, functions, "sin(e)-0.41")) < 0.01)
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025")) < 0.001)
self.assertTrue(abs(calc.evaluator(variables, functions, "e^(j*pi)") + 1) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "j||1") - 0.5 - 0.5j) < 0.00001)
variables['t'] = 1.0
# Use self.assertAlmostEqual here...
self.assertTrue(abs(calc.evaluator(variables, functions, "t") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T") - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "t", cs=True) - 1.0) < 0.00001)
self.assertTrue(abs(calc.evaluator(variables, functions, "T", cs=True) - 298) < 0.2)
# Use self.assertRaises here...
exception_happened = False
try:
calc.evaluator({}, {}, "5+7 QWSEKO")
except:
exception_happened = True
self.assertTrue(exception_happened)
try:
calc.evaluator({'r1': 5}, {}, "r1+r2")
except calc.UndefinedVariable:
pass
self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0)
exception_happened = False
try:
calc.evaluator(variables, functions, "r1*r3", cs=True)
except:
exception_happened = True
self.assertTrue(exception_happened)
class PostData(object):
"""Class which emulate postdata."""
def __init__(self, dict_data):
......
......@@ -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">
......
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();
Formula Equation Input
######################
Tag: ``<formulaequationinput />``
The formula equation input is a math input type used with Numerical and Formula
responses only. It is not to be used with Symoblic Response. It is comparable
to a ``<textline math="1"/>`` but with a different means to display the math.
It lets the platform validate the student's input as she types.
This is achieved by periodically sending the typed expression and requesting
its preview from the LMS. It parses the expression (using the same parser as
the parser it uses to eventually evaluate the response for grading), and sends
back an OK'd copy.
The basic appearance is that of a textbox with a preview box below it. The
student types in math (see note below for syntax), and a typeset preview
appears below it. Even complicated math expressions may be entered in.
For more information about the syntax, look in the course author's
documentation, under Appendix E, the section about Numerical Responses.
Format
******
The XML is rather simple, it is a ``<formulaequationinput />`` tag with an
optional ``size`` attribute, which defines the size (i.e. the width) of the
input box displayed to students for typing their math expression. Unlike
``<textline />``, there is no ``math`` attribute and adding such will have no
effect.
To see an example of the input type in context:
.. code-block:: xml
<problem>
<p>What base is the decimal numeral system in?</p>
<numericalresponse answer="10">
<formulaequationinput />
</numericalresponse>
<p>Write an expression for the product of R_1, R_2, and the inverse of R_3.</p>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="R_1*R_2/R_3">
<responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" />
</formularesponse>
</problem>
......@@ -30,6 +30,7 @@ Specific Problem Types
course_data_formats/custom_response.rst
course_data_formats/symbolic_response.rst
course_data_formats/jsinput.rst
course_data_formats/formula_equation_input.rst
Internal Data Formats
......
......@@ -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