Merge pull request #982 from MITx/feature/victor/inputtypes-refactor
Feature/victor/inputtypes refactor - addressed comments and added a few TODOs. Merging.
This commit is contained in:
@@ -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)
|
||||
|
||||
100
common/lib/capa/capa/customrender.py
Normal file
100
common/lib/capa/capa/customrender.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
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('<', '<'))
|
||||
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
|
||||
html.replace('<', '<'))
|
||||
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)
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
Module containing the problem elements which render into input objects
|
||||
|
||||
- textline
|
||||
- textbox (change this to textarea?)
|
||||
- schemmatic
|
||||
- choicegroup
|
||||
- radiogroup
|
||||
- checkboxgroup
|
||||
- textbox (aka codeinput)
|
||||
- schematic
|
||||
- choicegroup (aka radiogroup, checkboxgroup)
|
||||
- javascriptinput
|
||||
- imageinput (for clickable image)
|
||||
- optioninput (for option list)
|
||||
@@ -23,64 +21,34 @@ Each input type takes the xml tree as 'element', the previous answer as 'value',
|
||||
graded status as'status'
|
||||
"""
|
||||
|
||||
# TODO: rename "state" to "status" for all below. status is currently the answer for the
|
||||
# problem ID for the input element, but it will turn into a dict containing both the
|
||||
# answer and any associated message for the problem ID for the input element.
|
||||
# TODO: there is a lot of repetitive "grab these elements from xml attributes, with these defaults,
|
||||
# put them in the context" code. Refactor so class just specifies required and optional attrs (with
|
||||
# defaults for latter), and InputTypeBase does the right thing.
|
||||
|
||||
# TODO: Quoting and unquoting is handled in a pretty ad-hoc way. Also something that could be done
|
||||
# properly once in InputTypeBase.
|
||||
|
||||
# Possible todo: make inline the default for textlines and other "one-line" inputs. It probably
|
||||
# makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a
|
||||
# general css and layout strategy for capa, document it, then implement it.
|
||||
|
||||
|
||||
|
||||
import json
|
||||
import logging
|
||||
from lxml import etree
|
||||
import re
|
||||
import shlex # for splitting quoted strings
|
||||
import json
|
||||
|
||||
from lxml import etree
|
||||
import sys
|
||||
import xml.sax.saxutils as saxutils
|
||||
|
||||
from registry import TagRegistry
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
#########################################################################
|
||||
|
||||
_TAGS_TO_CLASSES = {}
|
||||
|
||||
def register_input_class(cls):
|
||||
"""
|
||||
Register cls as a supported input type. It is expected to have the same constructor as
|
||||
InputTypeBase, and to define cls.tags as a list of tags that it implements.
|
||||
|
||||
If an already-registered input type has claimed one of those tags, will raise ValueError.
|
||||
|
||||
If there are no tags in cls.tags, will also raise ValueError.
|
||||
"""
|
||||
|
||||
# Do all checks and complain before changing any state.
|
||||
if len(cls.tags) == 0:
|
||||
raise ValueError("No supported tags for class {0}".format(cls.__name__))
|
||||
|
||||
for t in cls.tags:
|
||||
if t in _TAGS_TO_CLASSES:
|
||||
other_cls = _TAGS_TO_CLASSES[t]
|
||||
if cls == other_cls:
|
||||
# registering the same class multiple times seems silly, but ok
|
||||
continue
|
||||
raise ValueError("Tag {0} already registered by class {1}. Can't register for class {2}"
|
||||
.format(t, other_cls.__name__, cls.__name__))
|
||||
|
||||
# Ok, should be good to change state now.
|
||||
for t in cls.tags:
|
||||
_TAGS_TO_CLASSES[t] = cls
|
||||
|
||||
def registered_input_tags():
|
||||
"""
|
||||
Get a list of all the xml tags that map to known input types.
|
||||
"""
|
||||
return _TAGS_TO_CLASSES.keys()
|
||||
|
||||
|
||||
def get_class_for_tag(tag):
|
||||
"""
|
||||
For any tag in registered_input_tags(), return the corresponding class. Otherwise, will raise KeyError.
|
||||
"""
|
||||
return _TAGS_TO_CLASSES[tag]
|
||||
|
||||
registry = TagRegistry()
|
||||
|
||||
class InputTypeBase(object):
|
||||
"""
|
||||
@@ -93,16 +61,18 @@ class InputTypeBase(object):
|
||||
"""
|
||||
Instantiate an InputType class. Arguments:
|
||||
|
||||
- system : ModuleSystem instance which provides OS, rendering, and user context. Specifically, must
|
||||
have a render_template function.
|
||||
- system : ModuleSystem instance which provides OS, rendering, and user context.
|
||||
Specifically, must have a render_template function.
|
||||
- xml : Element tree of this Input element
|
||||
- state : a dictionary with optional keys:
|
||||
* 'value'
|
||||
* 'id'
|
||||
* 'value' -- the current value of this input
|
||||
(what the student entered last time)
|
||||
* 'id' -- the id of this input, typically
|
||||
"{problem-location}_{response-num}_{input-num}"
|
||||
* 'status' (answered, unanswered, unsubmitted)
|
||||
* 'feedback' (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt. Specifically 'message', 'hint', 'hintmode'. If 'hintmode'
|
||||
is 'always', the hint is always displayed.)
|
||||
feedback from previous attempt. Specifically 'message', 'hint',
|
||||
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
|
||||
"""
|
||||
|
||||
self.xml = xml
|
||||
@@ -132,6 +102,26 @@ class InputTypeBase(object):
|
||||
|
||||
self.status = state.get('status', 'unanswered')
|
||||
|
||||
# Call subclass "constructor" -- means they don't have to worry about calling
|
||||
# super().__init__, and are isolated from changes to the input constructor interface.
|
||||
try:
|
||||
self.setup()
|
||||
except Exception as err:
|
||||
# Something went wrong: add xml to message, but keep the traceback
|
||||
msg = "Error in xml '{x}': {err} ".format(x=etree.tostring(xml), err=str(err))
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
InputTypes should override this to do any needed initialization. It is called after the
|
||||
constructor, so all base attributes will be set.
|
||||
|
||||
If this method raises an exception, it will be wrapped with a message that includes the
|
||||
problem xml.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_render_context(self):
|
||||
"""
|
||||
Abstract method. Subclasses should implement to return the dictionary
|
||||
@@ -146,40 +136,13 @@ class InputTypeBase(object):
|
||||
Return the html for this input, as an etree element.
|
||||
"""
|
||||
if self.template is None:
|
||||
raise NotImplementedError("no rendering template specified for class {0}".format(self.__class__))
|
||||
raise NotImplementedError("no rendering template specified for class {0}"
|
||||
.format(self.__class__))
|
||||
|
||||
html = self.system.render_template(self.template, self._get_render_context())
|
||||
return etree.XML(html)
|
||||
|
||||
|
||||
## TODO: Remove once refactor is complete
|
||||
def make_class_for_render_function(fn):
|
||||
"""
|
||||
Take an old-style render function, return a new-style input class.
|
||||
"""
|
||||
|
||||
class Impl(InputTypeBase):
|
||||
"""
|
||||
Inherit all the constructor logic from InputTypeBase...
|
||||
"""
|
||||
tags = [fn.__name__]
|
||||
def get_html(self):
|
||||
"""...delegate to the render function to do the work"""
|
||||
return fn(self.xml, self.value, self.status, self.system.render_template, self.msg)
|
||||
|
||||
# don't want all the classes to be called Impl (confuses register_input_class).
|
||||
Impl.__name__ = fn.__name__.capitalize()
|
||||
return Impl
|
||||
|
||||
|
||||
def _reg(fn):
|
||||
"""
|
||||
Register an old-style inputtype render function as a new-style subclass of InputTypeBase.
|
||||
This will go away once converting all input types to the new format is complete. (TODO)
|
||||
"""
|
||||
register_input_class(make_class_for_render_function(fn))
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -195,100 +158,98 @@ class OptionInput(InputTypeBase):
|
||||
template = "optioninput.html"
|
||||
tags = ['optioninput']
|
||||
|
||||
def setup(self):
|
||||
# Extract the options...
|
||||
options = self.xml.get('options')
|
||||
if not options:
|
||||
raise ValueError("optioninput: Missing 'options' specification.")
|
||||
|
||||
# parse the set of possible options
|
||||
oset = shlex.shlex(options[1:-1])
|
||||
oset.quotes = "'"
|
||||
oset.whitespace = ","
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# make ordered list with (key, value) same
|
||||
self.osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
|
||||
# TODO: allow ordering to be randomized
|
||||
|
||||
def _get_render_context(self):
|
||||
return _optioninput(self.xml, self.value, self.status, self.system.render_template, self.msg)
|
||||
|
||||
context = {
|
||||
'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
'options': self.osetdict,
|
||||
'inline': self.xml.get('inline',''),
|
||||
}
|
||||
return context
|
||||
|
||||
def optioninput(element, value, status, render_template, msg=''):
|
||||
context = _optioninput(element, value, status, render_template, msg)
|
||||
html = render_template("optioninput.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
def _optioninput(element, value, status, render_template, msg=''):
|
||||
"""
|
||||
Select option input type.
|
||||
|
||||
Example:
|
||||
|
||||
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
|
||||
"""
|
||||
eid = element.get('id')
|
||||
options = element.get('options')
|
||||
if not options:
|
||||
raise Exception(
|
||||
"[courseware.capa.inputtypes.optioninput] Missing options specification in "
|
||||
+ etree.tostring(element))
|
||||
|
||||
# parse the set of possible options
|
||||
oset = shlex.shlex(options[1:-1])
|
||||
oset.quotes = "'"
|
||||
oset.whitespace = ","
|
||||
oset = [x[1:-1] for x in list(oset)]
|
||||
|
||||
# make ordered list with (key, value) same
|
||||
osetdict = [(oset[x], oset[x]) for x in range(len(oset))]
|
||||
# TODO: allow ordering to be randomized
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'options': osetdict,
|
||||
'inline': element.get('inline',''),
|
||||
}
|
||||
return context
|
||||
|
||||
register_input_class(OptionInput)
|
||||
registry.register(OptionInput)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
# @register_render_function
|
||||
def choicegroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Radio button inputs: multiple choice or true/false
|
||||
|
||||
class ChoiceGroup(InputTypeBase):
|
||||
"""
|
||||
Radio button or checkbox inputs: multiple choice or true/false
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
if element.get('type') == "MultipleChoice":
|
||||
element_type = "radio"
|
||||
elif element.get('type') == "TrueFalse":
|
||||
element_type = "checkbox"
|
||||
else:
|
||||
element_type = "radio"
|
||||
choices = []
|
||||
for choice in element:
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.choicegroup] "
|
||||
"Error: only <choice> tags should be immediate children "
|
||||
"of a <choicegroup>, found %s instead" % choice.tag)
|
||||
ctext = ""
|
||||
# TODO: what if choice[0] has math tags in it?
|
||||
ctext += ''.join([etree.tostring(x) for x in choice])
|
||||
if choice.text is not None:
|
||||
# TODO: fix order?
|
||||
ctext += choice.text
|
||||
choices.append((choice.get("name"), ctext))
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': element_type,
|
||||
'choices': choices,
|
||||
'name_array_suffix': ''}
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(choicegroup)
|
||||
Example:
|
||||
|
||||
<choicegroup>
|
||||
<choice correct="false" name="foil1">
|
||||
<text>This is foil One.</text>
|
||||
</choice>
|
||||
<choice correct="false" name="foil2">
|
||||
<text>This is foil Two.</text>
|
||||
</choice>
|
||||
<choice correct="true" name="foil3">
|
||||
<text>This is foil Three.</text>
|
||||
</choice>
|
||||
</choicegroup>
|
||||
"""
|
||||
template = "choicegroup.html"
|
||||
tags = ['choicegroup', 'radiogroup', 'checkboxgroup']
|
||||
|
||||
def setup(self):
|
||||
# suffix is '' or [] to change the way the input is handled in --as a scalar or vector
|
||||
# value. (VS: would be nice to make this less hackish).
|
||||
if self.tag == 'choicegroup':
|
||||
self.suffix = ''
|
||||
self.element_type = "radio"
|
||||
elif self.tag == 'radiogroup':
|
||||
self.element_type = "radio"
|
||||
self.suffix = '[]'
|
||||
elif self.tag == 'checkboxgroup':
|
||||
self.element_type = "checkbox"
|
||||
self.suffix = '[]'
|
||||
else:
|
||||
raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag))
|
||||
|
||||
self.choices = extract_choices(self.xml)
|
||||
|
||||
def _get_render_context(self):
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'input_type': self.element_type,
|
||||
'choices': self.choices,
|
||||
'name_array_suffix': self.suffix}
|
||||
return context
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
def extract_choices(element):
|
||||
'''
|
||||
Extracts choices for a few input types, such as radiogroup and
|
||||
checkboxgroup.
|
||||
Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and
|
||||
CheckboxGroup.
|
||||
|
||||
returns list of (choice_name, choice_text) tuples
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
@@ -297,380 +258,258 @@ def extract_choices(element):
|
||||
choices = []
|
||||
|
||||
for choice in element:
|
||||
if not choice.tag == 'choice':
|
||||
raise Exception("[courseware.capa.inputtypes.extract_choices] \
|
||||
Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
if choice.tag != 'choice':
|
||||
raise Exception(
|
||||
"[capa.inputtypes.extract_choices] Expected a <choice> tag; got %s instead"
|
||||
% choice.tag)
|
||||
choice_text = ''.join([etree.tostring(x) for x in choice])
|
||||
if choice.text is not None:
|
||||
# TODO: fix order?
|
||||
choice_text += choice.text
|
||||
|
||||
choices.append((choice.get("name"), choice_text))
|
||||
|
||||
return choices
|
||||
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
def radiogroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Radio button inputs: (multiple choice)
|
||||
'''
|
||||
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'radio',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
registry.register(ChoiceGroup)
|
||||
|
||||
|
||||
_reg(radiogroup)
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
# TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of
|
||||
# desired semantics.
|
||||
def checkboxgroup(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Checkbox inputs: (select one or more choices)
|
||||
'''
|
||||
|
||||
eid = element.get('id')
|
||||
|
||||
choices = extract_choices(element)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'input_type': 'checkbox',
|
||||
'choices': choices,
|
||||
'name_array_suffix': '[]'}
|
||||
|
||||
html = render_template("choicegroup.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(checkboxgroup)
|
||||
|
||||
def javascriptinput(element, value, status, render_template, msg='null'):
|
||||
'''
|
||||
class JavascriptInput(InputTypeBase):
|
||||
"""
|
||||
Hidden field for javascript to communicate via; also loads the required
|
||||
scripts for rendering the problem and passes data to the problem.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
params = element.get('params')
|
||||
problem_state = element.get('problem_state')
|
||||
display_class = element.get('display_class')
|
||||
display_file = element.get('display_file')
|
||||
|
||||
# Need to provide a value that JSON can parse if there is no
|
||||
# student-supplied value yet.
|
||||
if value == "":
|
||||
value = 'null'
|
||||
TODO (arjun?): document this in detail. Initial notes:
|
||||
- display_class is a subclass of XProblemClassDisplay (see
|
||||
xmodule/xmodule/js/src/capa/display.coffee),
|
||||
- display_file is the js script to be in /static/js/ where display_class is defined.
|
||||
"""
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
msg = saxutils.escape(msg, escapedict)
|
||||
context = {'id': eid,
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,
|
||||
template = "javascriptinput.html"
|
||||
tags = ['javascriptinput']
|
||||
|
||||
def setup(self):
|
||||
# Need to provide a value that JSON can parse if there is no
|
||||
# student-supplied value yet.
|
||||
if self.value == "":
|
||||
self.value = 'null'
|
||||
|
||||
self.params = self.xml.get('params')
|
||||
self.problem_state = self.xml.get('problem_state')
|
||||
self.display_class = self.xml.get('display_class')
|
||||
self.display_file = self.xml.get('display_file')
|
||||
|
||||
|
||||
def _get_render_context(self):
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(self.value, escapedict)
|
||||
msg = saxutils.escape(self.msg, escapedict)
|
||||
|
||||
context = {'id': self.id,
|
||||
'params': self.params,
|
||||
'display_file': self.display_file,
|
||||
'display_class': self.display_class,
|
||||
'problem_state': self.problem_state,
|
||||
'value': value,
|
||||
'evaluation': msg,
|
||||
}
|
||||
html = render_template("javascriptinput.html", context)
|
||||
return etree.XML(html)
|
||||
return context
|
||||
|
||||
_reg(javascriptinput)
|
||||
registry.register(JavascriptInput)
|
||||
|
||||
|
||||
def textline(element, value, status, render_template, msg=""):
|
||||
'''
|
||||
Simple text line input, with optional size specification.
|
||||
'''
|
||||
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
if element.get('math') or element.get('dojs'):
|
||||
return textline_dynamath(element, value, status, render_template, msg)
|
||||
eid = element.get('id')
|
||||
if eid is None:
|
||||
msg = 'textline has no id: it probably appears outside of a known response type'
|
||||
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||
raise Exception(msg)
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
class TextLine(InputTypeBase):
|
||||
"""
|
||||
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
"""
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'inline': element.get('inline',''),
|
||||
template = "textline.html"
|
||||
tags = ['textline']
|
||||
|
||||
def setup(self):
|
||||
self.size = self.xml.get('size')
|
||||
|
||||
# if specified, then textline is hidden and input id is stored
|
||||
# in div with name=self.hidden.
|
||||
self.hidden = self.xml.get('hidden', False)
|
||||
|
||||
self.inline = self.xml.get('inline', False)
|
||||
|
||||
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
|
||||
self.do_math = bool(self.xml.get('math') or self.xml.get('dojs'))
|
||||
# TODO: do math checking using ajax instead of using js, so
|
||||
# that we only have one math parser.
|
||||
self.preprocessor = None
|
||||
if self.do_math:
|
||||
# Preprocessor to insert between raw input and Mathjax
|
||||
self.preprocessor = {'class_name': self.xml.get('preprocessorClassName',''),
|
||||
'script_src': self.xml.get('preprocessorSrc','')}
|
||||
if '' in self.preprocessor.values():
|
||||
self.preprocessor = None
|
||||
|
||||
|
||||
|
||||
def _get_render_context(self):
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(self.value, escapedict)
|
||||
|
||||
context = {'id': self.id,
|
||||
'value': value,
|
||||
'status': self.status,
|
||||
'size': self.size,
|
||||
'msg': self.msg,
|
||||
'hidden': self.hidden,
|
||||
'inline': self.inline,
|
||||
'do_math': self.do_math,
|
||||
'preprocessor': self.preprocessor,
|
||||
}
|
||||
return context
|
||||
|
||||
html = render_template("textinput.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
# TODO: needs to be self.system.DEBUG - but can't access system
|
||||
if True:
|
||||
log.debug('[inputtypes.textline] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
|
||||
_reg(textline)
|
||||
registry.register(TextLine)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class FileSubmission(InputTypeBase):
|
||||
"""
|
||||
Upload some files (e.g. for programming assignments)
|
||||
"""
|
||||
|
||||
def textline_dynamath(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Text line input with dynamic math display (equation rendered on client in real time
|
||||
during input).
|
||||
'''
|
||||
# TODO: Make a wrapper for <formulainput>
|
||||
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
|
||||
'''
|
||||
textline is used for simple one-line inputs, like formularesponse and symbolicresponse.
|
||||
uses a <span id=display_eid>`{::}`</span>
|
||||
and a hidden textarea with id=input_eid_fromjs for the mathjax rendering and return.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
template = "filesubmission.html"
|
||||
tags = ['filesubmission']
|
||||
|
||||
# Preprocessor to insert between raw input and Mathjax
|
||||
preprocessor = {'class_name': element.get('preprocessorClassName',''),
|
||||
'script_src': element.get('preprocessorSrc','')}
|
||||
if '' in preprocessor.values():
|
||||
preprocessor = None
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Your file(s) have been submitted; as soon as your submission is"
|
||||
" graded, this message will be replaced with the grader's feedback.")
|
||||
|
||||
# Escape characters in student input for safe XML parsing
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
def setup(self):
|
||||
escapedict = {'"': '"'}
|
||||
self.allowed_files = json.dumps(self.xml.get('allowed_files', '').split())
|
||||
self.allowed_files = saxutils.escape(self.allowed_files, escapedict)
|
||||
self.required_files = json.dumps(self.xml.get('required_files', '').split())
|
||||
self.required_files = saxutils.escape(self.required_files, escapedict)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'preprocessor': preprocessor,}
|
||||
html = render_template("textinput_dynamath.html", context)
|
||||
return etree.XML(html)
|
||||
# Check if problem has been queued
|
||||
queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if self.status == 'incomplete':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = FileSubmission.submitted_msg
|
||||
|
||||
def _get_render_context(self):
|
||||
|
||||
context = {'id': self.id,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
'value': self.value,
|
||||
'queue_len': self.queue_len,
|
||||
'allowed_files': self.allowed_files,
|
||||
'required_files': self.required_files,}
|
||||
return context
|
||||
|
||||
registry.register(FileSubmission)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
def filesubmission(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
Upload a single file (e.g. for programming assignments)
|
||||
'''
|
||||
eid = element.get('id')
|
||||
escapedict = {'"': '"'}
|
||||
allowed_files = json.dumps(element.get('allowed_files', '').split())
|
||||
allowed_files = saxutils.escape(allowed_files, escapedict)
|
||||
required_files = json.dumps(element.get('required_files', '').split())
|
||||
required_files = saxutils.escape(required_files, escapedict)
|
||||
|
||||
# Check if problem has been queued
|
||||
queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if status == 'incomplete':
|
||||
status = 'queued'
|
||||
queue_len = msg
|
||||
msg = "Your file(s) have been submitted; as soon as your submission is graded, this message will be replaced with the grader's feedback."
|
||||
class CodeInput(InputTypeBase):
|
||||
"""
|
||||
A text area input for code--uses codemirror, does syntax highlighting, special tab handling,
|
||||
etc.
|
||||
"""
|
||||
|
||||
context = { 'id': eid,
|
||||
'state': status,
|
||||
'msg': msg,
|
||||
'value': value,
|
||||
'queue_len': queue_len,
|
||||
'allowed_files': allowed_files,
|
||||
'required_files': required_files,}
|
||||
html = render_template("filesubmission.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(filesubmission)
|
||||
template = "codeinput.html"
|
||||
tags = ['codeinput',
|
||||
'textbox', # Another (older) name--at some point we may want to make it use a
|
||||
# non-codemirror editor.
|
||||
]
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
## TODO: Make a wrapper for <codeinput>
|
||||
def textbox(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
The textbox is used for code input. The message is the return HTML string from
|
||||
evaluating the code, eg error messages, and output from the code tests.
|
||||
def setup(self):
|
||||
self.rows = self.xml.get('rows') or '30'
|
||||
self.cols = self.xml.get('cols') or '80'
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
self.hidden = self.xml.get('hidden', '')
|
||||
|
||||
'''
|
||||
eid = element.get('id')
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
rows = element.get('rows') or '30'
|
||||
cols = element.get('cols') or '80'
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not self.value:
|
||||
self.value = self.xml.text
|
||||
|
||||
# if no student input yet, then use the default input given by the problem
|
||||
if not value:
|
||||
value = element.text
|
||||
# Check if problem has been queued
|
||||
self.queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if self.status == 'incomplete':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = 'Submitted to grader.'
|
||||
|
||||
# Check if problem has been queued
|
||||
queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of queue
|
||||
if status == 'incomplete':
|
||||
status = 'queued'
|
||||
queue_len = msg
|
||||
msg = 'Submitted to grader.'
|
||||
# For CodeMirror
|
||||
self.mode = self.xml.get('mode', 'python')
|
||||
self.linenumbers = self.xml.get('linenumbers', 'true')
|
||||
self.tabsize = int(self.xml.get('tabsize', '4'))
|
||||
|
||||
# For CodeMirror
|
||||
mode = element.get('mode','python')
|
||||
linenumbers = element.get('linenumbers','true')
|
||||
tabsize = element.get('tabsize','4')
|
||||
tabsize = int(tabsize)
|
||||
def _get_render_context(self):
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'hidden': hidden,
|
||||
'tabsize': tabsize,
|
||||
'queue_len': queue_len,
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
'mode': self.mode,
|
||||
'linenumbers': self.linenumbers,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'hidden': self.hidden,
|
||||
'tabsize': self.tabsize,
|
||||
'queue_len': self.queue_len,
|
||||
}
|
||||
html = render_template("textbox.html", context)
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
newmsg = 'error %s in rendering message' % (str(err).replace('<', '<'))
|
||||
newmsg += '<br/>Original message: %s' % msg.replace('<', '<')
|
||||
context['msg'] = newmsg
|
||||
html = render_template("textbox.html", context)
|
||||
xhtml = etree.XML(html)
|
||||
return xhtml
|
||||
return context
|
||||
|
||||
registry.register(CodeInput)
|
||||
|
||||
_reg(textbox)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
def schematic(element, value, status, render_template, msg=''):
|
||||
eid = element.get('id')
|
||||
height = element.get('height')
|
||||
width = element.get('width')
|
||||
parts = element.get('parts')
|
||||
analyses = element.get('analyses')
|
||||
initial_value = element.get('initial_value')
|
||||
submit_analyses = element.get('submit_analyses')
|
||||
context = {
|
||||
'id': eid,
|
||||
'value': value,
|
||||
'initial_value': initial_value,
|
||||
'state': status,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
html = render_template("schematicinput.html", context)
|
||||
return etree.XML(html)
|
||||
class Schematic(InputTypeBase):
|
||||
"""
|
||||
"""
|
||||
|
||||
_reg(schematic)
|
||||
template = "schematicinput.html"
|
||||
tags = ['schematic']
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
### TODO: Move out of inputtypes
|
||||
def math(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
This is not really an input type. It is a convention from Lon-CAPA, used for
|
||||
displaying a math equation.
|
||||
def setup(self):
|
||||
self.height = self.xml.get('height')
|
||||
self.width = self.xml.get('width')
|
||||
self.parts = self.xml.get('parts')
|
||||
self.analyses = self.xml.get('analyses')
|
||||
self.initial_value = self.xml.get('initial_value')
|
||||
self.submit_analyses = self.xml.get('submit_analyses')
|
||||
|
||||
Examples:
|
||||
|
||||
<m display="jsmath">$\displaystyle U(r)=4 U_0 </m>
|
||||
<m>$r_0$</m>
|
||||
def _get_render_context(self):
|
||||
|
||||
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'initial_value': self.initial_value,
|
||||
'status': self.status,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'parts': self.parts,
|
||||
'analyses': self.analyses,
|
||||
'submit_analyses': self.submit_analyses,}
|
||||
return context
|
||||
|
||||
TODO: use shorter tags (but this will require converting problem XML files!)
|
||||
'''
|
||||
mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text)
|
||||
mtag = 'mathjax'
|
||||
if not '\\displaystyle' in mathstr: mtag += 'inline'
|
||||
else: mathstr = mathstr.replace('\\displaystyle', '')
|
||||
mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
|
||||
|
||||
#if '\\displaystyle' in mathstr:
|
||||
# isinline = False
|
||||
# mathstr = mathstr.replace('\\displaystyle','')
|
||||
#else:
|
||||
# isinline = True
|
||||
# html = render_template("mathstring.html", {'mathstr':mathstr,
|
||||
# 'isinline':isinline,'tail':element.tail})
|
||||
|
||||
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
if False: # TODO needs to be self.system.DEBUG - but can't access system
|
||||
msg = '<html><div class="inline-error"><p>Error %s</p>' % str(err).replace('<', '<')
|
||||
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
|
||||
html.replace('<', '<'))
|
||||
msg += "</div></html>"
|
||||
log.error(msg)
|
||||
return etree.XML(msg)
|
||||
else:
|
||||
raise
|
||||
# xhtml.tail = element.tail # don't forget to include the tail!
|
||||
return xhtml
|
||||
|
||||
_reg(math)
|
||||
registry.register(Schematic)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def solution(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
This is not really an input type. It 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 sent with the HTML. It is obtained
|
||||
by an ajax call.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
size = element.get('size')
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
}
|
||||
html = render_template("solutionspan.html", context)
|
||||
return etree.XML(html)
|
||||
|
||||
_reg(solution)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def imageinput(element, value, status, render_template, msg=''):
|
||||
'''
|
||||
class ImageInput(InputTypeBase):
|
||||
"""
|
||||
Clickable image as an input field. Element should specify the image source, height,
|
||||
and width, e.g.
|
||||
|
||||
@@ -678,130 +517,117 @@ def imageinput(element, value, status, render_template, msg=''):
|
||||
|
||||
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle
|
||||
over acceptable area of image.
|
||||
'''
|
||||
eid = element.get('id')
|
||||
src = element.get('src')
|
||||
height = element.get('height')
|
||||
width = element.get('width')
|
||||
"""
|
||||
|
||||
# if value is of the form [x,y] then parse it and send along coordinates of previous answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', value.strip().replace(' ', ''))
|
||||
if m:
|
||||
(gx, gy) = [int(x) - 15 for x in m.groups()]
|
||||
else:
|
||||
(gx, gy) = (0, 0)
|
||||
template = "imageinput.html"
|
||||
tags = ['imageinput']
|
||||
|
||||
context = {
|
||||
'id': eid,
|
||||
'value': value,
|
||||
'height': height,
|
||||
'width': width,
|
||||
'src': src,
|
||||
'gx': gx,
|
||||
'gy': gy,
|
||||
'state': status, # to change
|
||||
'msg': msg, # to change
|
||||
}
|
||||
html = render_template("imageinput.html", context)
|
||||
return etree.XML(html)
|
||||
def setup(self):
|
||||
self.src = self.xml.get('src')
|
||||
self.height = self.xml.get('height')
|
||||
self.width = self.xml.get('width')
|
||||
|
||||
_reg(imageinput)
|
||||
# if value is of the form [x,y] then parse it and send along coordinates of previous answer
|
||||
m = re.match('\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', ''))
|
||||
if m:
|
||||
# Note: we subtract 15 to compensate for the size of the dot on the screen.
|
||||
# (is a 30x30 image--lms/static/green-pointer.png).
|
||||
(self.gx, self.gy) = [int(x) - 15 for x in m.groups()]
|
||||
else:
|
||||
(self.gx, self.gy) = (0, 0)
|
||||
|
||||
|
||||
def crystallography(element, value, status, render_template, msg=''):
|
||||
eid = element.get('id')
|
||||
if eid is None:
|
||||
msg = 'cryst has no id: it probably appears outside of a known response type'
|
||||
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||
raise Exception(msg)
|
||||
height = element.get('height')
|
||||
width = element.get('width')
|
||||
display_file = element.get('display_file')
|
||||
def _get_render_context(self):
|
||||
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'inline': element.get('inline', ''),
|
||||
'width': width,
|
||||
'height': height,
|
||||
'display_file': display_file,
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'height': self.height,
|
||||
'width': self.width,
|
||||
'src': self.src,
|
||||
'gx': self.gx,
|
||||
'gy': self.gy,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
}
|
||||
return context
|
||||
|
||||
html = render_template("crystallography.html", context)
|
||||
registry.register(ImageInput)
|
||||
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
# TODO: needs to be self.system.DEBUG - but can't access system
|
||||
if True:
|
||||
log.debug('[inputtypes.crystallography] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
_reg(crystallography)
|
||||
class Crystallography(InputTypeBase):
|
||||
"""
|
||||
An input for crystallography -- user selects 3 points on the axes, and we get a plane.
|
||||
|
||||
TODO: what's the actual value format?
|
||||
"""
|
||||
|
||||
template = "crystallography.html"
|
||||
tags = ['crystallography']
|
||||
|
||||
|
||||
def vsepr_input(element, value, status, render_template, msg=''):
|
||||
eid = element.get('id')
|
||||
if eid is None:
|
||||
msg = 'cryst has no id: it probably appears outside of a known response type'
|
||||
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
|
||||
raise Exception(msg)
|
||||
height = element.get('height')
|
||||
width = element.get('width')
|
||||
display_file = element.get('display_file')
|
||||
def setup(self):
|
||||
self.height = self.xml.get('height')
|
||||
self.width = self.xml.get('width')
|
||||
self.size = self.xml.get('size')
|
||||
|
||||
count = int(eid.split('_')[-2]) - 1 # HACK
|
||||
size = element.get('size')
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
hidden = element.get('hidden', '')
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
value = saxutils.escape(value, escapedict)
|
||||
# if specified, then textline is hidden and id is stored in div of name given by hidden
|
||||
self.hidden = self.xml.get('hidden', '')
|
||||
|
||||
molecules = element.get('molecules')
|
||||
geometries = element.get('geometries')
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
self.value = saxutils.escape(self.value, escapedict)
|
||||
|
||||
context = {'id': eid,
|
||||
'value': value,
|
||||
'state': status,
|
||||
'count': count,
|
||||
'size': size,
|
||||
'msg': msg,
|
||||
'hidden': hidden,
|
||||
'inline': element.get('inline', ''),
|
||||
'width': width,
|
||||
'height': height,
|
||||
'display_file': display_file,
|
||||
'molecules': molecules,
|
||||
'geometries': geometries,
|
||||
def _get_render_context(self):
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'size': self.size,
|
||||
'msg': self.msg,
|
||||
'hidden': self.hidden,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
}
|
||||
return context
|
||||
|
||||
html = render_template("vsepr_input.html", context)
|
||||
registry.register(Crystallography)
|
||||
|
||||
try:
|
||||
xhtml = etree.XML(html)
|
||||
except Exception as err:
|
||||
# TODO: needs to be self.system.DEBUG - but can't access system
|
||||
if True:
|
||||
log.debug('[inputtypes.vsepr_input] failed to parse XML for:\n%s' % html)
|
||||
raise
|
||||
return xhtml
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
_reg(vsepr_input)
|
||||
class VseprInput(InputTypeBase):
|
||||
"""
|
||||
Input for molecular geometry--show possible structures, let student
|
||||
pick structure and label positions with atoms or electron pairs.
|
||||
"""
|
||||
|
||||
template = 'vsepr_input.html'
|
||||
tags = ['vsepr_input']
|
||||
|
||||
def setup(self):
|
||||
self.height = self.xml.get('height')
|
||||
self.width = self.xml.get('width')
|
||||
|
||||
# Escape answers with quotes, so they don't crash the system!
|
||||
escapedict = {'"': '"'}
|
||||
self.value = saxutils.escape(self.value, escapedict)
|
||||
|
||||
self.molecules = self.xml.get('molecules')
|
||||
self.geometries = self.xml.get('geometries')
|
||||
|
||||
def _get_render_context(self):
|
||||
|
||||
context = {'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'msg': self.msg,
|
||||
'width': self.width,
|
||||
'height': self.height,
|
||||
'molecules': self.molecules,
|
||||
'geometries': self.geometries,
|
||||
}
|
||||
return context
|
||||
|
||||
registry.register(VseprInput)
|
||||
|
||||
#--------------------------------------------------------------------------------
|
||||
|
||||
@@ -820,15 +646,17 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
template = "chemicalequationinput.html"
|
||||
tags = ['chemicalequationinput']
|
||||
|
||||
def setup(self):
|
||||
self.size = self.xml.get('size', '20')
|
||||
|
||||
def _get_render_context(self):
|
||||
size = self.xml.get('size', '20')
|
||||
context = {
|
||||
'id': self.id,
|
||||
'value': self.value,
|
||||
'status': self.status,
|
||||
'size': size,
|
||||
'size': self.size,
|
||||
'previewer': '/static/js/capa/chemical_equation_preview.js',
|
||||
}
|
||||
return context
|
||||
|
||||
register_input_class(ChemicalEquationInput)
|
||||
registry.register(ChemicalEquationInput)
|
||||
|
||||
49
common/lib/capa/capa/registry.py
Normal file
49
common/lib/capa/capa/registry.py
Normal file
@@ -0,0 +1,49 @@
|
||||
class TagRegistry(object):
|
||||
"""
|
||||
A registry mapping tags to handlers.
|
||||
|
||||
(A dictionary with some extra error checking.)
|
||||
"""
|
||||
def __init__(self):
|
||||
self._mapping = {}
|
||||
|
||||
def register(self, cls):
|
||||
"""
|
||||
Register cls as a supported tag type. It is expected to define cls.tags as a list of tags
|
||||
that it implements.
|
||||
|
||||
If an already-registered type has registered one of those tags, will raise ValueError.
|
||||
|
||||
If there are no tags in cls.tags, will also raise ValueError.
|
||||
"""
|
||||
|
||||
# Do all checks and complain before changing any state.
|
||||
if len(cls.tags) == 0:
|
||||
raise ValueError("No tags specified for class {0}".format(cls.__name__))
|
||||
|
||||
for t in cls.tags:
|
||||
if t in self._mapping:
|
||||
other_cls = self._mapping[t]
|
||||
if cls == other_cls:
|
||||
# registering the same class multiple times seems silly, but ok
|
||||
continue
|
||||
raise ValueError("Tag {0} already registered by class {1}."
|
||||
" Can't register for class {2}"
|
||||
.format(t, other_cls.__name__, cls.__name__))
|
||||
|
||||
# Ok, should be good to change state now.
|
||||
for t in cls.tags:
|
||||
self._mapping[t] = cls
|
||||
|
||||
def registered_tags(self):
|
||||
"""
|
||||
Get a list of all the tags that have been registered.
|
||||
"""
|
||||
return self._mapping.keys()
|
||||
|
||||
def get_class_for_tag(self, tag):
|
||||
"""
|
||||
For any tag in registered_tags(), returns the corresponding class. Otherwise, will raise
|
||||
KeyError.
|
||||
"""
|
||||
return self._mapping[tag]
|
||||
@@ -1,12 +1,12 @@
|
||||
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
|
||||
<div class="indicator_container">
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
>${value|h}</textarea>
|
||||
|
||||
<div class="grader-status">
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif state == 'queued':
|
||||
% elif status == 'queued':
|
||||
<span class="processing" id="status_${id}">Queued</span>
|
||||
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
|
||||
% endif
|
||||
@@ -21,7 +21,7 @@
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
|
||||
<p class="debug">${state}</p>
|
||||
<p class="debug">${status}</p>
|
||||
</div>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
@@ -1,19 +1,19 @@
|
||||
<% doinline = "inline" if inline else "" %>
|
||||
|
||||
<section id="textinput_${id}" class="textinput ${doinline}" >
|
||||
<section id="inputtype_${id}" class="capa_inputtype" >
|
||||
<div id="holder" style="width:${width};height:${height}"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/raphael.js"></div><div class="script_placeholder" data-src="/static/js/sylvester.js"></div><div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/raphael.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
|
||||
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
|
||||
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<div class="unanswered ${doinline}" id="status_${id}">
|
||||
% elif state == 'correct':
|
||||
<div class="correct ${doinline}" id="status_${id}">
|
||||
% elif state == 'incorrect':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% elif state == 'incomplete':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% 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
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
@@ -29,13 +29,13 @@
|
||||
/>
|
||||
|
||||
<p class="status">
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
correct
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
@@ -45,7 +45,7 @@
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<section id="filesubmission_${id}" class="filesubmission">
|
||||
<div class="grader-status file">
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}">Correct</span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}">Incorrect</span>
|
||||
% elif state == 'queued':
|
||||
% elif status == 'queued':
|
||||
<span class="processing" id="status_${id}">Queued</span>
|
||||
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
|
||||
% endif
|
||||
<p class="debug">${state}</p>
|
||||
<p class="debug">${status}</p>
|
||||
|
||||
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" />
|
||||
</div>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% endif
|
||||
</span>
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
<textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea>
|
||||
% endif
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% endif
|
||||
% if msg:
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="correct" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
<span class="incorrect" id="status_${id}"></span>
|
||||
% endif
|
||||
</form>
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
</script>
|
||||
|
||||
<span id="answer_${id}"></span>
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
<span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
|
||||
% endif
|
||||
</span>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<% doinline = "inline" if inline else "" %>
|
||||
|
||||
<section id="textinput_${id}" class="textinput ${doinline}" >
|
||||
% if state == 'unsubmitted':
|
||||
<div class="unanswered ${doinline}" id="status_${id}">
|
||||
% elif state == 'correct':
|
||||
<div class="correct ${doinline}" id="status_${id}">
|
||||
% elif state == 'incorrect':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% elif state == 'incomplete':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
|
||||
<p class="status">
|
||||
% if state == 'unsubmitted':
|
||||
unanswered
|
||||
% elif state == 'correct':
|
||||
correct
|
||||
% elif state == 'incorrect':
|
||||
incorrect
|
||||
% elif state == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
@@ -1,50 +0,0 @@
|
||||
###
|
||||
### version of textline.html which does dynamic math
|
||||
###
|
||||
<section class="text-input-dynamath capa_inputtype" id="inputtype_${id}">
|
||||
|
||||
% if preprocessor is not None:
|
||||
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
|
||||
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
|
||||
% endif
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
% elif state == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
% elif state == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% elif state == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}"
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
<p class="status">
|
||||
% if state == 'unsubmitted':
|
||||
unanswered
|
||||
% elif state == 'correct':
|
||||
correct
|
||||
% elif state == 'incorrect':
|
||||
incorrect
|
||||
% elif state == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
<div id="display_${id}" class="equation">`{::}`</div>
|
||||
|
||||
</div>
|
||||
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea>
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
</section>
|
||||
64
common/lib/capa/capa/templates/textline.html
Normal file
64
common/lib/capa/capa/templates/textline.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<% doinline = "inline" if inline else "" %>
|
||||
|
||||
<section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline}" >
|
||||
|
||||
% if preprocessor is not None:
|
||||
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
|
||||
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
|
||||
% endif
|
||||
|
||||
% if status == 'unsubmitted':
|
||||
<div class="unanswered ${doinline}" id="status_${id}">
|
||||
% elif status == 'correct':
|
||||
<div class="correct ${doinline}" id="status_${id}">
|
||||
% elif status == 'incorrect':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% elif status == 'incomplete':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% endif
|
||||
|
||||
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
|
||||
% if do_math:
|
||||
class="math"
|
||||
% endif
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
/>
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif status == 'correct':
|
||||
correct
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
% if do_math:
|
||||
<div id="display_${id}" class="equation">`{::}`</div>
|
||||
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath">
|
||||
</textarea>
|
||||
|
||||
% endif
|
||||
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
|
||||
</section>
|
||||
@@ -1,6 +1,4 @@
|
||||
<% doinline = "inline" if inline else "" %>
|
||||
|
||||
<section id="textinput_${id}" class="textinput ${doinline}" >
|
||||
<section id="inputtype_${id}" class="capa_inputtype" >
|
||||
<table><tr><td height='600'>
|
||||
<div id="vsepr_div_${id}" style="position:relative;" data-molecules="${molecules}" data-geometries="${geometries}">
|
||||
<canvas id="vsepr_canvas_${id}" width="${width}" height="${height}">
|
||||
@@ -13,36 +11,28 @@
|
||||
|
||||
<div class="script_placeholder" data-src="/static/js/vsepr/vsepr.js"></div>
|
||||
|
||||
% if state == 'unsubmitted':
|
||||
<div class="unanswered ${doinline}" id="status_${id}">
|
||||
% elif state == 'correct':
|
||||
<div class="correct ${doinline}" id="status_${id}">
|
||||
% elif state == 'incorrect':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% elif state == 'incomplete':
|
||||
<div class="incorrect ${doinline}" id="status_${id}">
|
||||
% endif
|
||||
% if hidden:
|
||||
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
|
||||
% 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}" value="${value}"
|
||||
% if size:
|
||||
size="${size}"
|
||||
% endif
|
||||
% if hidden:
|
||||
style="display:none;"
|
||||
% endif
|
||||
style="display:none;"
|
||||
/>
|
||||
|
||||
<p class="status">
|
||||
% if state == 'unsubmitted':
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif state == 'correct':
|
||||
% elif status == 'correct':
|
||||
correct
|
||||
% elif state == 'incorrect':
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif state == 'incomplete':
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
@@ -52,7 +42,7 @@
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
@@ -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")),
|
||||
|
||||
76
common/lib/capa/capa/tests/test_customrender.py
Normal file
76
common/lib/capa/capa/tests/test_customrender.py
Normal file
@@ -0,0 +1,76 @@
|
||||
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.
|
||||
|
||||
@@ -1,50 +1,30 @@
|
||||
"""
|
||||
Tests of input types (and actually responsetypes too)
|
||||
Tests of input types.
|
||||
|
||||
TODO:
|
||||
- test unicode in values, parameters, etc.
|
||||
- test various html escapes
|
||||
- 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
|
||||
|
||||
class OptionInputTest(unittest.TestCase):
|
||||
'''
|
||||
Make sure option inputs work
|
||||
'''
|
||||
def test_rendering_new(self):
|
||||
xml = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
|
||||
element = etree.fromstring(xml)
|
||||
|
||||
value = 'Down'
|
||||
status = 'answered'
|
||||
context = inputtypes._optioninput(element, value, status, test_system.render_template)
|
||||
print 'context: ', context
|
||||
|
||||
expected = {'value': 'Down',
|
||||
'options': [('Up', 'Up'), ('Down', 'Down')],
|
||||
'state': 'answered',
|
||||
'msg': '',
|
||||
'inline': '',
|
||||
'id': 'sky_input'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_rendering(self):
|
||||
xml_str = """<optioninput options="('Up','Down')" id="sky_input" correct="Up"/>"""
|
||||
@@ -53,16 +33,451 @@ class OptionInputTest(unittest.TestCase):
|
||||
state = {'value': 'Down',
|
||||
'id': 'sky_input',
|
||||
'status': 'answered'}
|
||||
option_input = inputtypes.OptionInput(system, element, state)
|
||||
option_input = lookup_tag('optioninput')(test_system, element, state)
|
||||
|
||||
context = option_input._get_render_context()
|
||||
|
||||
expected = {'value': 'Down',
|
||||
'options': [('Up', 'Up'), ('Down', 'Down')],
|
||||
'state': 'answered',
|
||||
'status': 'answered',
|
||||
'msg': '',
|
||||
'inline': '',
|
||||
'id': 'sky_input'}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
class ChoiceGroupTest(unittest.TestCase):
|
||||
'''
|
||||
Test choice groups, radio groups, and checkbox groups
|
||||
'''
|
||||
|
||||
def check_group(self, tag, expected_input_type, expected_suffix):
|
||||
xml_str = """
|
||||
<{tag}>
|
||||
<choice correct="false" name="foil1"><text>This is foil One.</text></choice>
|
||||
<choice correct="false" name="foil2"><text>This is foil Two.</text></choice>
|
||||
<choice correct="true" name="foil3">This is foil Three.</choice>
|
||||
</{tag}>
|
||||
""".format(tag=tag)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'foil3',
|
||||
'id': 'sky_input',
|
||||
'status': 'answered'}
|
||||
|
||||
the_input = lookup_tag(tag)(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'sky_input',
|
||||
'value': 'foil3',
|
||||
'status': 'answered',
|
||||
'input_type': expected_input_type,
|
||||
'choices': [('foil1', '<text>This is foil One.</text>'),
|
||||
('foil2', '<text>This is foil Two.</text>'),
|
||||
('foil3', 'This is foil Three.'),],
|
||||
'name_array_suffix': expected_suffix, # what is this for??
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_choicegroup(self):
|
||||
self.check_group('choicegroup', 'radio', '')
|
||||
|
||||
def test_radiogroup(self):
|
||||
self.check_group('radiogroup', 'radio', '[]')
|
||||
|
||||
def test_checkboxgroup(self):
|
||||
self.check_group('checkboxgroup', 'checkbox', '[]')
|
||||
|
||||
|
||||
|
||||
class JavascriptInputTest(unittest.TestCase):
|
||||
'''
|
||||
The javascript input is a pretty straightforward pass-thru, but test it anyway
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
params = "(1,2,3)"
|
||||
|
||||
problem_state = "abc12',12&hi<there>"
|
||||
display_class = "a_class"
|
||||
display_file = "my_files/hi.js"
|
||||
|
||||
xml_str = """<javascriptinput id="prob_1_2" params="{params}" problem_state="{ps}"
|
||||
display_class="{dc}" display_file="{df}"/>""".format(
|
||||
params=params,
|
||||
ps=quote_attr(problem_state),
|
||||
dc=display_class, df=display_file)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': '3',}
|
||||
the_input = lookup_tag('javascriptinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'params': params,
|
||||
'display_file': display_file,
|
||||
'display_class': display_class,
|
||||
'problem_state': problem_state,
|
||||
'value': '3',
|
||||
'evaluation': '',}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class TextLineTest(unittest.TestCase):
|
||||
'''
|
||||
Check that textline inputs work, with and without math.
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
size = "42"
|
||||
xml_str = """<textline id="prob_1_2" size="{size}"/>""".format(size=size)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'BumbleBee',
|
||||
'status': 'unanswered',
|
||||
'size': size,
|
||||
'msg': '',
|
||||
'hidden': False,
|
||||
'inline': False,
|
||||
'do_math': False,
|
||||
'preprocessor': None}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_math_rendering(self):
|
||||
size = "42"
|
||||
preprocessorClass = "preParty"
|
||||
script = "foo/party.js"
|
||||
|
||||
xml_str = """<textline math="True" id="prob_1_2" size="{size}"
|
||||
preprocessorClassName="{pp}"
|
||||
preprocessorSrc="{sc}"/>""".format(size=size, pp=preprocessorClass, sc=script)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'BumbleBee',}
|
||||
the_input = lookup_tag('textline')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'BumbleBee',
|
||||
'status': 'unanswered',
|
||||
'size': size,
|
||||
'msg': '',
|
||||
'hidden': False,
|
||||
'inline': False,
|
||||
'do_math': True,
|
||||
'preprocessor': {'class_name': preprocessorClass,
|
||||
'script_src': script}}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class FileSubmissionTest(unittest.TestCase):
|
||||
'''
|
||||
Check that file submission inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
allowed_files = "runme.py nooooo.rb ohai.java"
|
||||
required_files = "cookies.py"
|
||||
|
||||
xml_str = """<filesubmission id="prob_1_2"
|
||||
allowed_files="{af}"
|
||||
required_files="{rf}"
|
||||
/>""".format(af=allowed_files,
|
||||
rf=required_files,)
|
||||
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
esc = lambda s: saxutils.escape(s, escapedict)
|
||||
|
||||
state = {'value': 'BumbleBee.py',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
input_class = lookup_tag('filesubmission')
|
||||
the_input = input_class(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'value': 'BumbleBee.py',
|
||||
'queue_len': '3',
|
||||
'allowed_files': esc('["runme.py", "nooooo.rb", "ohai.java"]'),
|
||||
'required_files': esc('["cookies.py"]')}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class CodeInputTest(unittest.TestCase):
|
||||
'''
|
||||
Check that codeinput inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
mode = "parrot"
|
||||
linenumbers = 'false'
|
||||
rows = '37'
|
||||
cols = '11'
|
||||
tabsize = '7'
|
||||
|
||||
xml_str = """<codeinput id="prob_1_2"
|
||||
mode="{m}"
|
||||
cols="{c}"
|
||||
rows="{r}"
|
||||
linenumbers="{ln}"
|
||||
tabsize="{ts}"
|
||||
/>""".format(m=mode, c=cols, r=rows, ln=linenumbers, ts=tabsize)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
escapedict = {'"': '"'}
|
||||
esc = lambda s: saxutils.escape(s, escapedict)
|
||||
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'feedback' : {'message': '3'}, }
|
||||
|
||||
the_input = lookup_tag('codeinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': 'Submitted to grader.',
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
'hidden': '',
|
||||
'tabsize': int(tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class SchematicTest(unittest.TestCase):
|
||||
'''
|
||||
Check that schematic inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
height = '12'
|
||||
width = '33'
|
||||
parts = 'resistors, capacitors, and flowers'
|
||||
analyses = 'fast, slow, and pink'
|
||||
initial_value = 'two large batteries'
|
||||
submit_analyses = 'maybe'
|
||||
|
||||
|
||||
xml_str = """<schematic id="prob_1_2"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
parts="{p}"
|
||||
analyses="{a}"
|
||||
initial_value="{iv}"
|
||||
submit_analyses="{sa}"
|
||||
/>""".format(h=height, w=width, p=parts, a=analyses,
|
||||
iv=initial_value, sa=submit_analyses)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
value = 'three resistors and an oscilating pendulum'
|
||||
state = {'value': value,
|
||||
'status': 'unsubmitted'}
|
||||
|
||||
the_input = lookup_tag('schematic')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'initial_value': initial_value,
|
||||
'status': 'unsubmitted',
|
||||
'width': width,
|
||||
'height': height,
|
||||
'parts': parts,
|
||||
'analyses': analyses,
|
||||
'submit_analyses': submit_analyses,
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class ImageInputTest(unittest.TestCase):
|
||||
'''
|
||||
Check that image inputs work
|
||||
'''
|
||||
|
||||
def check(self, value, egx, egy):
|
||||
height = '78'
|
||||
width = '427'
|
||||
src = 'http://www.edx.org/cowclicker.jpg'
|
||||
|
||||
xml_str = """<imageinput id="prob_1_2"
|
||||
src="{s}"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
/>""".format(s=src, h=height, w=width)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': value,
|
||||
'status': 'unsubmitted'}
|
||||
|
||||
the_input = lookup_tag('imageinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'status': 'unsubmitted',
|
||||
'width': width,
|
||||
'height': height,
|
||||
'src': src,
|
||||
'gx': egx,
|
||||
'gy': egy,
|
||||
'msg': ''}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
def test_with_value(self):
|
||||
# Check that compensating for the dot size works properly.
|
||||
self.check('[50,40]', 35, 25)
|
||||
|
||||
def test_without_value(self):
|
||||
self.check('', 0, 0)
|
||||
|
||||
def test_corrupt_values(self):
|
||||
self.check('[12', 0, 0)
|
||||
self.check('[12, a]', 0, 0)
|
||||
self.check('[12 10]', 0, 0)
|
||||
self.check('[12]', 0, 0)
|
||||
self.check('[12 13 14]', 0, 0)
|
||||
|
||||
|
||||
|
||||
class CrystallographyTest(unittest.TestCase):
|
||||
'''
|
||||
Check that crystallography inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
height = '12'
|
||||
width = '33'
|
||||
size = '10'
|
||||
|
||||
xml_str = """<crystallography id="prob_1_2"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
size="{s}"
|
||||
/>""".format(h=height, w=width, s=size)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
value = 'abc'
|
||||
state = {'value': value,
|
||||
'status': 'unsubmitted'}
|
||||
|
||||
the_input = lookup_tag('crystallography')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'status': 'unsubmitted',
|
||||
'size': size,
|
||||
'msg': '',
|
||||
'hidden': '',
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
class VseprTest(unittest.TestCase):
|
||||
'''
|
||||
Check that vsepr inputs work
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
height = '12'
|
||||
width = '33'
|
||||
molecules = "H2O, C2O"
|
||||
geometries = "AX12,TK421"
|
||||
|
||||
xml_str = """<vsepr id="prob_1_2"
|
||||
height="{h}"
|
||||
width="{w}"
|
||||
molecules="{m}"
|
||||
geometries="{g}"
|
||||
/>""".format(h=height, w=width, m=molecules, g=geometries)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
value = 'abc'
|
||||
state = {'value': value,
|
||||
'status': 'unsubmitted'}
|
||||
|
||||
the_input = lookup_tag('vsepr_input')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': value,
|
||||
'status': 'unsubmitted',
|
||||
'msg': '',
|
||||
'width': width,
|
||||
'height': height,
|
||||
'molecules': molecules,
|
||||
'geometries': geometries,
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
|
||||
class ChemicalEquationTest(unittest.TestCase):
|
||||
'''
|
||||
Check that chemical equation inputs work.
|
||||
'''
|
||||
|
||||
def test_rendering(self):
|
||||
size = "42"
|
||||
xml_str = """<chemicalequationinput id="prob_1_2" size="{size}"/>""".format(size=size)
|
||||
|
||||
element = etree.fromstring(xml_str)
|
||||
|
||||
state = {'value': 'H2OYeah',}
|
||||
the_input = lookup_tag('chemicalequationinput')(test_system, element, state)
|
||||
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'H2OYeah',
|
||||
'status': 'unanswered',
|
||||
'size': size,
|
||||
'previewer': '/static/js/capa/chemical_equation_preview.js',
|
||||
}
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user