Commit 90196eb0 by Peter Baratta

Create a new response type for Numerical/Formula

Named `FormulaEquationInput` (name up for debate)
-For the initial pass, just copy off of 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
parent 31d8fa15
"""
Previewing capability for FormulaResponse and NumericalResponse
"""
def latex_preview(string, variables=None, functions=None, case_sensitive=False):
"""
Render the math string into latex for previewing.
-`variables` is an interable of valid user-defined variables
-`functions` is one of functions
-`case_sensitive` tells how to match variables in the string to these user-
defined sets.
TODO actually have a preview here
"""
return u"{len}\\text{{ chars in: \"{str}\"}}".format(
str=string,
len=len(string)
)
# -*- coding: utf-8 -*-
"""
Unit tests for preview.py
"""
import unittest
import preview
class PreviewTest(unittest.TestCase):
"""
Run tests for preview.latex_preview
"""
def test_method_works(self):
"""
Test that no exceptions are thrown and something returns
"""
result = preview.latex_preview(u"✖^2+1/2")
self.assertTrue(len(result) > 0)
...@@ -47,6 +47,7 @@ import pyparsing ...@@ -47,6 +47,7 @@ import pyparsing
from .registry import TagRegistry from .registry import TagRegistry
from chem import chemcalc from chem import chemcalc
from preview import latex_preview
import xqueue_interface import xqueue_interface
from datetime import datetime from datetime import datetime
...@@ -1056,6 +1057,82 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -1056,6 +1057,82 @@ class ChemicalEquationInput(InputTypeBase):
registry.register(ChemicalEquationInput) 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 this once we have a standard way of requiring js to be loaded.
"""
return {'previewer': '/static/js/capa/formula_equation_preview.js', }
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 html preview of a formula formula or equation. get should
contain a key 'formula' and value 'some formula string'.
Returns a json dictionary:
{
'preview' : 'the-preview-html' or ''
'error' : 'the-error' or ''
'request-start' : <time sent with request>
}
"""
result = {'preview': '',
'error': ''}
formula = get['formula']
if formula is None:
result['error'] = "No formula specified."
return result
result['request-start'] = int(get['request-start'])
try:
result['preview'] = latex_preview(formula) # TODO add references to variables, etc&
except pyparsing.ParseException as err:
result['error'] = u"Couldn't parse formula: {0}".format(err.message)
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)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -1692,7 +1692,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1692,7 +1692,7 @@ class FormulaResponse(LoncapaResponse):
response_tag = 'formularesponse' response_tag = 'formularesponse'
hint_tag = 'formulahint' hint_tag = 'formulahint'
allowed_inputfields = ['textline'] allowed_inputfields = ['textline', 'formulaequationinput']
required_attributes = ['answer', 'samples'] required_attributes = ['answer', 'samples']
max_inputfields = 1 max_inputfields = 1
......
<section id="formulaequationinput_${id}" class="formulaequationinput">
<div class="script_placeholder" data-src="${previewer}"/>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" data-input-id="${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<div id="input_${id}_preview" class="equation">\[\]
</div>
<p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
# -*- coding: utf-8 -*-
""" """
Tests of input types. Tests of input types.
...@@ -23,7 +24,8 @@ import xml.sax.saxutils as saxutils ...@@ -23,7 +24,8 @@ import xml.sax.saxutils as saxutils
from . import test_system from . import test_system
from capa import inputtypes from capa import inputtypes
from mock import ANY from mock import ANY, patch
from pyparsing import ParseException
# just a handy shortcut # just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag lookup_tag = inputtypes.registry.get_class_for_tag
...@@ -732,6 +734,97 @@ class ChemicalEquationTest(unittest.TestCase): ...@@ -732,6 +734,97 @@ class ChemicalEquationTest(unittest.TestCase):
self.assertEqual(response['error'], "") self.assertEqual(response['error'], "")
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()
expected = {'id': 'prob_1_2',
'value': 'x^2+1/2',
'status': 'unanswered',
'msg': '',
'size': self.size,
'previewer': '/static/js/capa/formula_equation_preview.js',
}
self.assertEqual(context, expected)
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.assertTrue('preview' in 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",
{'formula': None, 'request-start': 1, }
)
self.assertTrue('error' in 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(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ")
response = self.the_input.handle_ajax(
"preview_formcalc",
{'formula': 'x^2+1/2', 'request-start': 1, }
)
self.assertTrue('error' in 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 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.assertTrue('error' in response)
self.assertEqual(response['error'], "Error while rendering preview")
class DragAndDropTest(unittest.TestCase): class DragAndDropTest(unittest.TestCase):
''' '''
Check that drag and drop inputs work Check that drag and drop inputs work
......
(function () {
update = function() {
function create_handler(saved_div) {
return (function(response) {
if (saved_div.data('last-response') > response['request-start']) {
return;
}
else {
saved_div.data('last-response', response['request-start']);
}
var jax = MathJax.Hub.getAllJax(saved_div[0])[0];
var math_code;
if (response.error) {
math_code = "\text{" + response.error + "}";
//saved_div.html("<span class='error'>" + response.error + "</span>");
} else {
math_code = response.preview;
//saved_div.html(response.preview);
}
MathJax.Hub.Queue(['Text', jax, math_code],
['Reprocess', jax]);
});
}
prev_id = "#" + this.id + "_preview";
preview_div = $(prev_id);
// find the closest parent problems-wrapper and use that url
url = $(this).closest('.problems-wrapper').data('url');
// grab the input id from the input
input_id = $(this).data('input-id')
Problem.inputAjax(url, input_id, 'preview_formcalc', {
"formula" : this.value,
"request-start" : Date.now()
}, create_handler(preview_div));
}
inputs = $('.formulaequationinput input');
// set last-response to 0 on all inputs
inputs.each(function () {
prev_id = "#" + this.id + "_preview";
$(prev_id).data('last-response', 0);
});
// update on load
inputs.each(update);
// and on every change
inputs.bind("input", update);
}).call(this);
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