Commit d36d8cba by Victor Shnayder

Cleanups:

 - switch to using registry
 - pull math and solution "input types" into a separate customrender.py file
 - make capa_problem use new custom renderers and input types registries
 - remove unused imports, methods, etc
 - add tests for math and solution tags.
parent f6637b7f
......@@ -38,6 +38,7 @@ import calc
from correctmap import CorrectMap
import eia
import inputtypes
import customrender
from util import contextualize_text, convert_files_to_filenames
import xqueue_interface
......@@ -47,23 +48,8 @@ import responsetypes
# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])
# Different ways students can input code
entry_types = ['textline',
'schematic',
'textbox',
'imageinput',
'optioninput',
'choicegroup',
'radiogroup',
'checkboxgroup',
'filesubmission',
'javascriptinput',
'crystallography',
'chemicalequationinput',
'vsepr_input']
# extra things displayed after "show answers" is pressed
solution_types = ['solution']
solution_tags = ['solution']
# these get captured as student responses
response_properties = ["codeparam", "responseparam", "answer"]
......@@ -309,7 +295,7 @@ class LoncapaProblem(object):
answer_map.update(results)
# include solutions from <solution>...</solution> stanzas
for entry in self.tree.xpath("//" + "|//".join(solution_types)):
for entry in self.tree.xpath("//" + "|//".join(solution_tags)):
answer = etree.tostring(entry)
if answer:
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
......@@ -487,7 +473,7 @@ class LoncapaProblem(object):
problemid = problemtree.get('id') # my ID
if problemtree.tag in inputtypes.registered_input_tags():
if problemtree.tag in inputtypes.registry.registered_tags():
# If this is an inputtype subtree, let it render itself.
status = "unsubmitted"
msg = ''
......@@ -513,7 +499,7 @@ class LoncapaProblem(object):
'hint': hint,
'hintmode': hintmode,}}
input_type_cls = inputtypes.get_class_for_tag(problemtree.tag)
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
the_input = input_type_cls(self.system, problemtree, state)
return the_input.get_html()
......@@ -521,9 +507,15 @@ class LoncapaProblem(object):
if problemtree in self.responders:
return self.responders[problemtree].render_html(self._extract_html)
# let each custom renderer render itself:
if problemtree.tag in customrender.registry.registered_tags():
renderer_class = customrender.registry.get_class_for_tag(problemtree.tag)
renderer = renderer_class(self.system, problemtree)
return renderer.get_html()
# otherwise, render children recursively, and copy over attributes
tree = etree.Element(problemtree.tag)
for item in problemtree:
# render child recursively
item_xhtml = self._extract_html(item)
if item_xhtml is not None:
tree.append(item_xhtml)
......@@ -560,11 +552,12 @@ class LoncapaProblem(object):
response_id += 1
answer_id = 1
input_tags = inputtypes.registry.registered_tags()
inputfields = tree.xpath("|".join(['//' + response.tag + '[@id=$id]//' + x
for x in (entry_types + solution_types)]),
for x in (input_tags + solution_tags)]),
id=response_id_str)
# assign one answer_id for each entry_type or solution_type
# assign one answer_id for each input type or solution type
for entry in inputfields:
entry.attrib['response_id'] = str(response_id)
entry.attrib['answer_id'] = str(answer_id)
......
"""
This has custom renderers: classes that know how to render certain problem tags (e.g. <math> and
<solution>) to html.
These tags do not have state, so they just get passed the system (for access to render_template),
and the xml element.
"""
from registry import TagRegistry
import logging
import re
import shlex # for splitting quoted strings
import json
from lxml import etree
import xml.sax.saxutils as saxutils
from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__)
registry = TagRegistry()
#-----------------------------------------------------------------------------
class MathRenderer(object):
tags = ['math']
def __init__(self, system, xml):
'''
Render math using latex-like formatting.
Examples:
<math>$\displaystyle U(r)=4 U_0 $</math>
<math>$r_0$</math>
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
TODO: use shorter tags (but this will require converting problem XML files!)
'''
self.system = system
self.xml = xml
mathstr = re.sub('\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text)
mtag = 'mathjax'
if not r'\displaystyle' in mathstr:
mtag += 'inline'
else:
mathstr = mathstr.replace(r'\displaystyle', '')
self.mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""
# TODO: why are there nested html tags here?? Why are there html tags at all, in fact?
html = '<html><html>%s</html><html>%s</html></html>' % (
self.mathstr, saxutils.escape(self.xml.tail))
try:
xhtml = etree.XML(html)
except Exception as err:
if self.system.DEBUG:
msg = '<html><div class="inline-error"><p>Error %s</p>' % (
str(err).replace('<', '&lt;'))
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml
registry.register(MathRenderer)
#-----------------------------------------------------------------------------
class SolutionRenderer(object):
'''
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
extended answer (a problem "solution") after "show answers" is pressed.
Note that the solution content is NOT rendered and returned in the HTML. It is obtained by an
ajax call.
'''
tags = ['solution']
def __init__(self, system, xml):
self.system = system
self.id = xml.get('id')
def get_html(self):
context = {'id': self.id}
html = self.system.render_template("solutionspan.html", context)
return etree.XML(html)
registry.register(SolutionRenderer)
......@@ -4,13 +4,23 @@ import os
from mock import Mock
import xml.sax.saxutils as saxutils
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring
the template name. To make the output valid xml, quotes the content, and wraps it in a <div>
"""
return '<div>{0}</div>'.format(saxutils.escape(repr(context)))
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
......
from lxml import etree
import unittest
import xml.sax.saxutils as saxutils
from . import test_system
from capa import customrender
# just a handy shortcut
lookup_tag = customrender.registry.get_class_for_tag
def extract_context(xml):
"""
Given an xml element corresponding to the output of test_system.render_template, get back the
original context
"""
return eval(xml.text)
def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class HelperTest(unittest.TestCase):
'''
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system.render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
self.check({})
self.check({1, 2})
self.check({'id', 'an id'})
self.check({'with"quote', 'also"quote'})
class SolutionRenderTest(unittest.TestCase):
'''
Make sure solutions render properly.
'''
def test_rendering(self):
solution = 'To compute unicorns, count them.'
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system, element)
self.assertEqual(renderer.id, 'solution_12')
# our test_system "renders" templates to a div with the repr of the context
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id' : 'solution_12'})
class MathRenderTest(unittest.TestCase):
'''
Make sure math renders properly.
'''
def check_parse(self, latex_in, mathjax_out):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system, element)
self.assertEqual(renderer.mathstr, mathjax_out)
def test_parsing(self):
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
self.check_parse('$abc', '$abc')
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
"""
Tests of input types (and actually responsetypes too).
Tests of input types.
TODO:
- test unicode in values, parameters, etc.
......@@ -7,27 +7,16 @@ TODO:
- test funny xml chars -- should never get xml parse error if things are escaped properly.
"""
from datetime import datetime
import json
from mock import Mock
from nose.plugins.skip import SkipTest
import os
from lxml import etree
import unittest
import xml.sax.saxutils as saxutils
from . import test_system
from capa import inputtypes
from lxml import etree
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring the template name.
"""
return repr(context)
# just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag
system = Mock(render_template=tst_render_template)
def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
......@@ -44,7 +33,7 @@ class OptionInputTest(unittest.TestCase):
state = {'value': 'Down',
'id': 'sky_input',
'status': 'answered'}
option_input = inputtypes.get_class_for_tag('optioninput')(system, element, state)
option_input = lookup_tag('optioninput')(test_system, element, state)
context = option_input._get_render_context()
......@@ -80,7 +69,7 @@ class ChoiceGroupTest(unittest.TestCase):
'id': 'sky_input',
'status': 'answered'}
option_input = inputtypes.get_class_for_tag('choicegroup')(system, element, state)
option_input = lookup_tag('choicegroup')(test_system, element, state)
context = option_input._get_render_context()
......@@ -119,7 +108,7 @@ class ChoiceGroupTest(unittest.TestCase):
'id': 'sky_input',
'status': 'answered'}
the_input = inputtypes.get_class_for_tag(tag)(system, element, state)
the_input = lookup_tag(tag)(test_system, element, state)
context = the_input._get_render_context()
......@@ -164,7 +153,7 @@ class JavascriptInputTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': '3',}
the_input = inputtypes.get_class_for_tag('javascriptinput')(system, element, state)
the_input = lookup_tag('javascriptinput')(test_system, element, state)
context = the_input._get_render_context()
......@@ -191,7 +180,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',}
the_input = inputtypes.get_class_for_tag('textline')(system, element, state)
the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context()
......@@ -219,7 +208,7 @@ class TextLineTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'BumbleBee',}
the_input = inputtypes.get_class_for_tag('textline')(system, element, state)
the_input = lookup_tag('textline')(test_system, element, state)
context = the_input._get_render_context()
......@@ -260,7 +249,7 @@ class FileSubmissionTest(unittest.TestCase):
state = {'value': 'BumbleBee.py',
'status': 'incomplete',
'feedback' : {'message': '3'}, }
the_input = inputtypes.get_class_for_tag('filesubmission')(system, element, state)
the_input = lookup_tag('filesubmission')(test_system, element, state)
context = the_input._get_render_context()
......@@ -304,7 +293,7 @@ class CodeInputTest(unittest.TestCase):
'status': 'incomplete',
'feedback' : {'message': '3'}, }
the_input = inputtypes.get_class_for_tag('codeinput')(system, element, state)
the_input = lookup_tag('codeinput')(test_system, element, state)
context = the_input._get_render_context()
......@@ -354,7 +343,7 @@ class SchematicTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = inputtypes.get_class_for_tag('schematic')(system, element, state)
the_input = lookup_tag('schematic')(test_system, element, state)
context = the_input._get_render_context()
......@@ -393,7 +382,7 @@ class ImageInputTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = inputtypes.get_class_for_tag('imageinput')(system, element, state)
the_input = lookup_tag('imageinput')(test_system, element, state)
context = the_input._get_render_context()
......@@ -447,7 +436,7 @@ class CrystallographyTest(unittest.TestCase):
state = {'value': value,
'status': 'unsubmitted'}
the_input = inputtypes.get_class_for_tag('crystallography')(system, element, state)
the_input = lookup_tag('crystallography')(test_system, element, state)
context = the_input._get_render_context()
......@@ -476,7 +465,7 @@ class ChemicalEquationTest(unittest.TestCase):
element = etree.fromstring(xml_str)
state = {'value': 'H2OYeah',}
the_input = inputtypes.get_class_for_tag('chemicalequationinput')(system, element, state)
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
context = the_input._get_render_context()
......
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