Commit 7fcf02a0 by Victor Shnayder

Further refactor

- small Attribute and InputTypeBase interface changes to make things cleaner
- move html quoting into templates (use ${blah | h} syntax)
- converting input types to use new format.
parent 74e23546
...@@ -57,7 +57,7 @@ class Attribute(object): ...@@ -57,7 +57,7 @@ class Attribute(object):
# want to allow default to be None, but also allow required objects # want to allow default to be None, but also allow required objects
_sentinel = object() _sentinel = object()
def __init__(self, name, default=_sentinel, transform=None, validate=None): def __init__(self, name, default=_sentinel, transform=None, validate=None, render=True):
""" """
Define an attribute Define an attribute
...@@ -72,11 +72,14 @@ class Attribute(object): ...@@ -72,11 +72,14 @@ class Attribute(object):
validate (function str-or-return-type-of-tranform -> unit or exception): If not None, called to validate the validate (function str-or-return-type-of-tranform -> unit or exception): If not None, called to validate the
(possibly transformed) value of the attribute. Should raise ValueError with a helpful message if (possibly transformed) value of the attribute. Should raise ValueError with a helpful message if
the value is invalid. the value is invalid.
render (bool): if False, don't include this attribute in the template context.
""" """
self.name = name self.name = name
self.default = default self.default = default
self.validate = validate self.validate = validate
self.transform = transform self.transform = transform
self.render = render
def parse_from_xml(self, element): def parse_from_xml(self, element):
""" """
...@@ -171,8 +174,7 @@ class InputTypeBase(object): ...@@ -171,8 +174,7 @@ class InputTypeBase(object):
""" """
Should return a list of Attribute objects (see docstring there for details). Subclasses should override. e.g. Should return a list of Attribute objects (see docstring there for details). Subclasses should override. e.g.
return super(MyClass, cls).attributes + [Attribute('unicorn', True), return [Attribute('unicorn', True), Attribute('num_dragons', 12, transform=int), ...]
Attribute('num_dragons', 12, transform=int), ...]
""" """
return [] return []
...@@ -183,14 +185,19 @@ class InputTypeBase(object): ...@@ -183,14 +185,19 @@ class InputTypeBase(object):
function parses the input xml and pulls out those attributes. This function parses the input xml and pulls out those attributes. This
isolates most simple input types from needing to deal with xml parsing at all. isolates most simple input types from needing to deal with xml parsing at all.
Processes attributes, putting the results in the self.loaded_attributes dictionary. Processes attributes, putting the results in the self.loaded_attributes dictionary. Also creates a set
self.to_render, containing the names of attributes that should be included in the context by default.
""" """
# Use a local dict so that if there are exceptions, we don't end up in a partially-initialized state. # Use local dicts and sets so that if there are exceptions, we don't end up in a partially-initialized state.
d = {} loaded = {}
to_render = set()
for a in self.get_attributes(): for a in self.get_attributes():
d[a.name] = a.parse_from_xml(self.xml) loaded[a.name] = a.parse_from_xml(self.xml)
if a.render:
to_render.add(a.name)
self.loaded_attributes = d self.loaded_attributes = loaded
self.to_render = to_render
def setup(self): def setup(self):
""" """
...@@ -209,11 +216,11 @@ class InputTypeBase(object): ...@@ -209,11 +216,11 @@ class InputTypeBase(object):
(Separate from get_html to faciliate testing of logic separately from the rendering) (Separate from get_html to faciliate testing of logic separately from the rendering)
The default implementation gets the following rendering context: basic things like value, id, The default implementation gets the following rendering context: basic things like value, id, status, and msg,
status, and msg, as well as everything in self.loaded_attributes. as well as everything in self.loaded_attributes, and everything returned by self._extra_context().
This means that input types that only parse attributes get everything they need, and don't need This means that input types that only parse attributes and pass them to the template get everything they need,
to override this method. and don't need to override this method.
""" """
context = { context = {
'id': self.id, 'id': self.id,
...@@ -221,9 +228,17 @@ class InputTypeBase(object): ...@@ -221,9 +228,17 @@ class InputTypeBase(object):
'status': self.status, 'status': self.status,
'msg': self.msg, 'msg': self.msg,
} }
context.update(self.loaded_attributes) context.update((a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render)
context.update(self._extra_context())
return context return context
def _extra_context(self):
"""
Subclasses can override this to return extra context that should be passed to their templates for rendering.
This is useful when the input type requires computing new template variables from the parsed attributes.
"""
return {}
def get_html(self): def get_html(self):
""" """
...@@ -233,8 +248,7 @@ class InputTypeBase(object): ...@@ -233,8 +248,7 @@ class InputTypeBase(object):
raise NotImplementedError("no rendering template specified for class {0}" raise NotImplementedError("no rendering template specified for class {0}"
.format(self.__class__)) .format(self.__class__))
context = self._default_render_context() context = self._get_render_context()
context.update(self._get_render_context())
html = self.system.render_template(self.template, context) html = self.system.render_template(self.template, context)
return etree.XML(html) return etree.XML(html)
...@@ -255,27 +269,31 @@ class OptionInput(InputTypeBase): ...@@ -255,27 +269,31 @@ class OptionInput(InputTypeBase):
template = "optioninput.html" template = "optioninput.html"
tags = ['optioninput'] tags = ['optioninput']
@classmethod @staticmethod
def get_attributes(cls):
"""
Convert options to a convenient format.
"""
def parse_options(options): def parse_options(options):
"""Given options string, convert it into an ordered list of (option, option) tuples """
(Why? I don't know--that's what the template uses at the moment) Given options string, convert it into an ordered list of (option_id, option_description) tuples, where
id==description for now. TODO: make it possible to specify different id and descriptions.
""" """
# parse the set of possible options # parse the set of possible options
oset = shlex.shlex(options[1:-1]) lexer = shlex.shlex(options[1:-1])
oset.quotes = "'" lexer.quotes = "'"
oset.whitespace = "," # Allow options to be separated by whitespace as well as commas
oset = [x[1:-1] for x in list(oset)] lexer.whitespace = ", "
# make ordered list with (key, value) same # remove quotes
return [(oset[x], oset[x]) for x in range(len(oset))] tokens = [x[1:-1] for x in list(lexer)]
return super(OptionInput, cls).get_attributes() + [ # make list of (option_id, option_description), with description=id
Attribute('options', transform=parse_options), return [(t, t) for t in tokens]
@classmethod
def get_attributes(cls):
"""
Convert options to a convenient format.
"""
return [Attribute('options', transform=cls.parse_options),
Attribute('inline', '')] Attribute('inline', '')]
registry.register(OptionInput) registry.register(OptionInput)
...@@ -315,26 +333,22 @@ class ChoiceGroup(InputTypeBase): ...@@ -315,26 +333,22 @@ class ChoiceGroup(InputTypeBase):
# value. (VS: would be nice to make this less hackish). # value. (VS: would be nice to make this less hackish).
if self.tag == 'choicegroup': if self.tag == 'choicegroup':
self.suffix = '' self.suffix = ''
self.element_type = "radio" self.html_input_type = "radio"
elif self.tag == 'radiogroup': elif self.tag == 'radiogroup':
self.element_type = "radio" self.html_input_type = "radio"
self.suffix = '[]' self.suffix = '[]'
elif self.tag == 'checkboxgroup': elif self.tag == 'checkboxgroup':
self.element_type = "checkbox" self.html_input_type = "checkbox"
self.suffix = '[]' self.suffix = '[]'
else: else:
raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag)) raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag))
self.choices = extract_choices(self.xml) self.choices = extract_choices(self.xml)
def _get_render_context(self): def _extra_context(self):
context = {'id': self.id, return {'input_type': self.html_input_type,
'value': self.value,
'status': self.status,
'input_type': self.element_type,
'choices': self.choices, 'choices': self.choices,
'name_array_suffix': self.suffix} 'name_array_suffix': self.suffix}
return context
def extract_choices(element): def extract_choices(element):
''' '''
...@@ -384,33 +398,23 @@ class JavascriptInput(InputTypeBase): ...@@ -384,33 +398,23 @@ class JavascriptInput(InputTypeBase):
template = "javascriptinput.html" template = "javascriptinput.html"
tags = ['javascriptinput'] tags = ['javascriptinput']
@classmethod
def get_attributes(cls):
"""
Register the attributes.
"""
return [Attribute('params', None),
Attribute('problem_state', None),
Attribute('display_class', None),
Attribute('display_file', None),]
def setup(self): def setup(self):
# Need to provide a value that JSON can parse if there is no # Need to provide a value that JSON can parse if there is no
# student-supplied value yet. # student-supplied value yet.
if self.value == "": if self.value == "":
self.value = 'null' 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,
}
return context
registry.register(JavascriptInput) registry.register(JavascriptInput)
...@@ -418,51 +422,53 @@ registry.register(JavascriptInput) ...@@ -418,51 +422,53 @@ registry.register(JavascriptInput)
class TextLine(InputTypeBase): class TextLine(InputTypeBase):
""" """
A text line input. Can do math preview if "math"="1" is specified.
""" """
template = "textline.html" template = "textline.html"
tags = ['textline'] tags = ['textline']
def setup(self):
self.size = self.xml.get('size')
# if specified, then textline is hidden and input id is stored @classmethod
# in div with name=self.hidden. def get_attributes(cls):
self.hidden = self.xml.get('hidden', False) """
Register the attributes.
"""
return [
Attribute('size', None),
self.inline = self.xml.get('inline', False) # if specified, then textline is hidden and input id is stored
# in div with name=self.hidden. (TODO: is this functionality used by anyone?)
Attribute('hidden', False),
Attribute('inline', False),
# Attributes below used in setup(), not rendered directly.
Attribute('math', None, render=False),
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x # TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
self.do_math = bool(self.xml.get('math') or self.xml.get('dojs')) Attribute('dojs', None, render=False),
Attribute('preprocessorClassName', None, render=False),
Attribute('preprocessorSrc', None, render=False),
]
def setup(self):
self.do_math = bool(self.loaded_attributes['math'] or
self.loaded_attributes['dojs'])
# TODO: do math checking using ajax instead of using js, so # TODO: do math checking using ajax instead of using js, so
# that we only have one math parser. # that we only have one math parser.
self.preprocessor = None self.preprocessor = None
if self.do_math: if self.do_math:
# Preprocessor to insert between raw input and Mathjax # Preprocessor to insert between raw input and Mathjax
self.preprocessor = {'class_name': self.xml.get('preprocessorClassName',''), self.preprocessor = {'class_name': self.loaded_attributes['preprocessorClassName'],
'script_src': self.xml.get('preprocessorSrc','')} 'script_src': self.loaded_attributes['preprocessorSrc']}
if '' in self.preprocessor.values(): if None in self.preprocessor.values():
self.preprocessor = None self.preprocessor = None
def _extra_context(self):
def _get_render_context(self): return {'do_math': self.do_math,
# Escape answers with quotes, so they don't crash the system! 'preprocessor': self.preprocessor,}
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
registry.register(TextLine) registry.register(TextLine)
...@@ -480,13 +486,26 @@ class FileSubmission(InputTypeBase): ...@@ -480,13 +486,26 @@ class FileSubmission(InputTypeBase):
submitted_msg = ("Your file(s) have been submitted; as soon as your submission is" 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.") " graded, this message will be replaced with the grader's feedback.")
def setup(self): @staticmethod
escapedict = {'"': '"'} def parse_files(files):
self.allowed_files = json.dumps(self.xml.get('allowed_files', '').split()) """
self.allowed_files = saxutils.escape(self.allowed_files, escapedict) Given a string like 'a.py b.py c.out', split on whitespace and return as a json list.
self.required_files = json.dumps(self.xml.get('required_files', '').split()) """
self.required_files = saxutils.escape(self.required_files, escapedict) return json.dumps(files.split())
@classmethod
def get_attributes(cls):
"""
Convert the list of allowed files to a convenient format.
"""
return [Attribute('allowed_files', '[]', transform=cls.parse_files),
Attribute('required_files', '[]', transform=cls.parse_files),]
def setup(self):
"""
Do some magic to handle queueing status (render as "queued" instead of "incomplete"),
pull queue_len from the msg field. (TODO: get rid of the queue_len hack).
"""
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0 self.queue_len = 0
# Flag indicating that the problem has been queued, 'msg' is length of queue # Flag indicating that the problem has been queued, 'msg' is length of queue
...@@ -495,15 +514,8 @@ class FileSubmission(InputTypeBase): ...@@ -495,15 +514,8 @@ class FileSubmission(InputTypeBase):
self.queue_len = self.msg self.queue_len = self.msg
self.msg = FileSubmission.submitted_msg self.msg = FileSubmission.submitted_msg
def _get_render_context(self): def _extra_context(self):
return {'queue_len': self.queue_len,}
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 return context
registry.register(FileSubmission) registry.register(FileSubmission)
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
% endif % endif
<p class="debug">${status}</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}"/> <input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files|h}" data-allowed_files="${allowed_files|h}"/>
</div> </div>
<div class="message">${msg|n}</div> <div class="message">${msg|n}</div>
</section> </section>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/> <input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
<div class="javascriptinput_data" data-display_class="${display_class}" <div class="javascriptinput_data" data-display_class="${display_class}"
data-problem_state="${problem_state}" data-params="${params}" data-problem_state="${problem_state}" data-params="${params}"
data-submission="${value}" data-evaluation="${evaluation}"> data-submission="${value|h}" data-evaluation="${msg|h}">
</div> </div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div> <div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div> <div class="javascriptinput_container"></div>
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" <input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if do_math: % if do_math:
class="math" class="math"
% endif % endif
......
...@@ -46,6 +46,19 @@ class OptionInputTest(unittest.TestCase): ...@@ -46,6 +46,19 @@ class OptionInputTest(unittest.TestCase):
self.assertEqual(context, expected) self.assertEqual(context, expected)
def test_option_parsing(self):
f = inputtypes.OptionInput.parse_options
def check(input, options):
"""Take list of options, confirm that output is in the silly doubled format"""
expected = [(o, o) for o in options]
self.assertEqual(f(input), expected)
check("('a','b')", ['a', 'b'])
check("('a', 'b')", ['a', 'b'])
check("('a b','b')", ['a b', 'b'])
check("('My \"quoted\"place','b')", ['My \"quoted\"place', 'b'])
class ChoiceGroupTest(unittest.TestCase): class ChoiceGroupTest(unittest.TestCase):
''' '''
Test choice groups, radio groups, and checkbox groups Test choice groups, radio groups, and checkbox groups
...@@ -73,6 +86,7 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -73,6 +86,7 @@ class ChoiceGroupTest(unittest.TestCase):
expected = {'id': 'sky_input', expected = {'id': 'sky_input',
'value': 'foil3', 'value': 'foil3',
'status': 'answered', 'status': 'answered',
'msg': '',
'input_type': expected_input_type, 'input_type': expected_input_type,
'choices': [('foil1', '<text>This is foil One.</text>'), 'choices': [('foil1', '<text>This is foil One.</text>'),
('foil2', '<text>This is foil Two.</text>'), ('foil2', '<text>This is foil Two.</text>'),
...@@ -119,12 +133,13 @@ class JavascriptInputTest(unittest.TestCase): ...@@ -119,12 +133,13 @@ class JavascriptInputTest(unittest.TestCase):
context = the_input._get_render_context() context = the_input._get_render_context()
expected = {'id': 'prob_1_2', expected = {'id': 'prob_1_2',
'status': 'unanswered',
'msg': '',
'value': '3',
'params': params, 'params': params,
'display_file': display_file, 'display_file': display_file,
'display_class': display_class, 'display_class': display_class,
'problem_state': problem_state, 'problem_state': problem_state,}
'value': '3',
'evaluation': '',}
self.assertEqual(context, expected) self.assertEqual(context, expected)
...@@ -204,9 +219,6 @@ class FileSubmissionTest(unittest.TestCase): ...@@ -204,9 +219,6 @@ class FileSubmissionTest(unittest.TestCase):
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
escapedict = {'"': '&quot;'}
esc = lambda s: saxutils.escape(s, escapedict)
state = {'value': 'BumbleBee.py', state = {'value': 'BumbleBee.py',
'status': 'incomplete', 'status': 'incomplete',
'feedback' : {'message': '3'}, } 'feedback' : {'message': '3'}, }
...@@ -220,8 +232,8 @@ class FileSubmissionTest(unittest.TestCase): ...@@ -220,8 +232,8 @@ class FileSubmissionTest(unittest.TestCase):
'msg': input_class.submitted_msg, 'msg': input_class.submitted_msg,
'value': 'BumbleBee.py', 'value': 'BumbleBee.py',
'queue_len': '3', 'queue_len': '3',
'allowed_files': esc('["runme.py", "nooooo.rb", "ohai.java"]'), 'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': esc('["cookies.py"]')} 'required_files': '["cookies.py"]'}
self.assertEqual(context, expected) self.assertEqual(context, expected)
......
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