diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index de29b5e664..0b2250f98d 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -21,26 +21,26 @@ Each input type takes the xml tree as 'element', the previous answer as 'value', graded status as'status' """ -# 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: make hints do something + +# TODO: make all inputtypes actually render msg + +# TODO: remove unused fields (e.g. 'hidden' in a few places) + +# TODO: add validators so that content folks get better error messages. -# 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. - - +from collections import namedtuple import json import logging from lxml import etree import re import shlex # for splitting quoted strings import sys -import xml.sax.saxutils as saxutils from registry import TagRegistry @@ -50,6 +50,61 @@ log = logging.getLogger('mitx.' + __name__) registry = TagRegistry() +class Attribute(object): + """ + Allows specifying required and optional attributes for input types. + """ + + # want to allow default to be None, but also allow required objects + _sentinel = object() + + def __init__(self, name, default=_sentinel, transform=None, validate=None, render=True): + """ + Define an attribute + + name (str): then name of the attribute--should be alphanumeric (valid for an XML attribute) + + default (any type): If not specified, this attribute is required. If specified, use this as the default value + if the attribute is not specified. Note that this value will not be transformed or validated. + + transform (function str -> any type): If not None, will be called to transform the parsed value into an internal + representation. + + 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 + the value is invalid. + + render (bool): if False, don't include this attribute in the template context. + """ + self.name = name + self.default = default + self.validate = validate + self.transform = transform + self.render = render + + def parse_from_xml(self, element): + """ + Given an etree xml element that should have this attribute, do the obvious thing: + - look for it. raise ValueError if not found and required. + - transform and validate. pass through any exceptions from transform or validate. + """ + val = element.get(self.name) + if self.default == self._sentinel and val is None: + raise ValueError('Missing required attribute {0}.'.format(self.name)) + + if val is None: + # not required, so return default + return self.default + + if self.transform is not None: + val = self.transform(val) + + if self.validate is not None: + self.validate(val) + + return val + + class InputTypeBase(object): """ Abstract base class for input types. @@ -102,9 +157,12 @@ 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: + # Pre-parse and propcess all the declared requirements. + self.process_requirements() + + # Call subclass "constructor" -- means they don't have to worry about calling + # super().__init__, and are isolated from changes to the input constructor interface. self.setup() except Exception as err: # Something went wrong: add xml to message, but keep the traceback @@ -112,6 +170,36 @@ class InputTypeBase(object): raise Exception, msg, sys.exc_info()[2] + @classmethod + def get_attributes(cls): + """ + Should return a list of Attribute objects (see docstring there for details). Subclasses should override. e.g. + + return [Attribute('unicorn', True), Attribute('num_dragons', 12, transform=int), ...] + """ + return [] + + + def process_requirements(self): + """ + Subclasses can declare lists of required and optional 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. + + 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 local dicts and sets so that if there are exceptions, we don't end up in a partially-initialized state. + loaded = {} + to_render = set() + for a in self.get_attributes(): + loaded[a.name] = a.parse_from_xml(self.xml) + if a.render: + to_render.add(a.name) + + self.loaded_attributes = loaded + self.to_render = to_render + def setup(self): """ InputTypes should override this to do any needed initialization. It is called after the @@ -122,14 +210,36 @@ class InputTypeBase(object): """ pass + def _get_render_context(self): """ - Abstract method. Subclasses should implement to return the dictionary - of keys needed to render their template. + Should return a dictionary of keys needed to render the template for the input type. (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, status, and msg, + as well as everything in self.loaded_attributes, and everything returned by self._extra_context(). + + This means that input types that only parse attributes and pass them to the template get everything they need, + and don't need to override this method. """ - raise NotImplementedError + context = { + 'id': self.id, + 'value': self.value, + 'status': self.status, + 'msg': self.msg, + } + 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 + + 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): """ @@ -139,7 +249,9 @@ class InputTypeBase(object): raise NotImplementedError("no rendering template specified for class {0}" .format(self.__class__)) - html = self.system.render_template(self.template, self._get_render_context()) + context = self._get_render_context() + + html = self.system.render_template(self.template, context) return etree.XML(html) @@ -153,38 +265,38 @@ class OptionInput(InputTypeBase): Example: The location of the sky + + # TODO: allow ordering to be randomized """ 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.") - + @staticmethod + def parse_options(options): + """ + 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 - oset = shlex.shlex(options[1:-1]) - oset.quotes = "'" - oset.whitespace = "," - oset = [x[1:-1] for x in list(oset)] + lexer = shlex.shlex(options[1:-1]) + lexer.quotes = "'" + # Allow options to be separated by whitespace as well as commas + lexer.whitespace = ", " - # 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 + # remove quotes + tokens = [x[1:-1] for x in list(lexer)] - def _get_render_context(self): + # make list of (option_id, option_description), with description=id + return [(t, t) for t in tokens] - context = { - 'id': self.id, - 'value': self.value, - 'status': self.status, - 'msg': self.msg, - 'options': self.osetdict, - 'inline': self.xml.get('inline',''), - } - return context + @classmethod + def get_attributes(cls): + """ + Convert options to a convenient format. + """ + return [Attribute('options', transform=cls.parse_options), + Attribute('inline', '')] registry.register(OptionInput) @@ -223,53 +335,50 @@ class ChoiceGroup(InputTypeBase): # value. (VS: would be nice to make this less hackish). if self.tag == 'choicegroup': self.suffix = '' - self.element_type = "radio" + self.html_input_type = "radio" elif self.tag == 'radiogroup': - self.element_type = "radio" + self.html_input_type = "radio" self.suffix = '[]' elif self.tag == 'checkboxgroup': - self.element_type = "checkbox" + self.html_input_type = "checkbox" self.suffix = '[]' else: raise Exception("ChoiceGroup: unexpected tag {0}".format(self.tag)) - self.choices = extract_choices(self.xml) + self.choices = self.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 _extra_context(self): + return {'input_type': self.html_input_type, + 'choices': self.choices, + 'name_array_suffix': self.suffix} -def extract_choices(element): - ''' - Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and - CheckboxGroup. + @staticmethod + def extract_choices(element): + ''' + Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and + CheckboxGroup. - returns list of (choice_name, choice_text) tuples + 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. - ''' + TODO: allow order of choices to be randomized, following lon-capa spec. Use + "location" attribute, ie random, top, bottom. + ''' - choices = [] + choices = [] - for choice in element: - if choice.tag != 'choice': - raise Exception( - "[capa.inputtypes.extract_choices] Expected a 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 + for choice in element: + if choice.tag != 'choice': + raise Exception( + "[capa.inputtypes.extract_choices] Expected a 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)) + choices.append((choice.get("name"), choice_text)) - return choices + return choices registry.register(ChoiceGroup) @@ -292,33 +401,23 @@ class JavascriptInput(InputTypeBase): template = "javascriptinput.html" 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): # 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, - } - return context - registry.register(JavascriptInput) @@ -326,51 +425,55 @@ registry.register(JavascriptInput) class TextLine(InputTypeBase): """ + A text line input. Can do math preview if "math"="1" is specified. + If the hidden attribute is specified, the textline is hidden and the input id is stored in a div with name equal + to the value of the hidden attribute. This is used e.g. for embedding simulations turned into questions. """ template = "textline.html" tags = ['textline'] + + @classmethod + def get_attributes(cls): + """ + Register the attributes. + """ + return [ + Attribute('size', None), + + + 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 + Attribute('dojs', None, render=False), + Attribute('preprocessorClassName', None, render=False), + Attribute('preprocessorSrc', None, render=False), + ] + + def setup(self): - self.size = self.xml.get('size') + self.do_math = bool(self.loaded_attributes['math'] or + self.loaded_attributes['dojs']) - # 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 = {'class_name': self.loaded_attributes['preprocessorClassName'], + 'script_src': self.loaded_attributes['preprocessorSrc']} + if None 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 + def _extra_context(self): + return {'do_math': self.do_math, + 'preprocessor': self.preprocessor,} registry.register(TextLine) @@ -388,13 +491,26 @@ class FileSubmission(InputTypeBase): 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.") - 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) + @staticmethod + def parse_files(files): + """ + Given a string like 'a.py b.py c.out', split on whitespace and return as a json list. + """ + 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 self.queue_len = 0 # Flag indicating that the problem has been queued, 'msg' is length of queue @@ -403,15 +519,8 @@ class FileSubmission(InputTypeBase): 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,} + def _extra_context(self): + return {'queue_len': self.queue_len,} return context registry.register(FileSubmission) @@ -431,13 +540,30 @@ class CodeInput(InputTypeBase): # non-codemirror editor. ] + # pulled out for testing + submitted_msg = ("Submitted. As soon as your submission is" + " graded, this message will be replaced with the grader's feedback.") + + @classmethod + def get_attributes(cls): + """ + Convert options to a convenient format. + """ + return [Attribute('rows', '30'), + Attribute('cols', '80'), + Attribute('hidden', ''), + + # For CodeMirror + Attribute('mode', 'python'), + Attribute('linenumbers', 'true'), + # Template expects tabsize to be an int it can do math with + Attribute('tabsize', 4, transform=int), + ] 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', '') - + """ + Implement special logic: handle queueing state, and default input. + """ # if no student input yet, then use the default input given by the problem if not self.value: self.value = self.xml.text @@ -448,28 +574,11 @@ class CodeInput(InputTypeBase): if self.status == 'incomplete': self.status = 'queued' self.queue_len = self.msg - self.msg = 'Submitted to grader.' + self.msg = self.submitted_msg - # For CodeMirror - self.mode = self.xml.get('mode', 'python') - self.linenumbers = self.xml.get('linenumbers', 'true') - self.tabsize = int(self.xml.get('tabsize', '4')) - - def _get_render_context(self): - - 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, - } - return context + def _extra_context(self): + """Defined queue_len, add it """ + return {'queue_len': self.queue_len,} registry.register(CodeInput) @@ -482,26 +591,19 @@ class Schematic(InputTypeBase): template = "schematicinput.html" tags = ['schematic'] - 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') + @classmethod + def get_attributes(cls): + """ + Convert options to a convenient format. + """ + return [ + Attribute('height', None), + Attribute('width', None), + Attribute('parts', None), + Attribute('analyses', None), + Attribute('initial_value', None), + Attribute('submit_analyses', None),] - - def _get_render_context(self): - - 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 registry.register(Schematic) @@ -522,12 +624,20 @@ class ImageInput(InputTypeBase): template = "imageinput.html" tags = ['imageinput'] - def setup(self): - self.src = self.xml.get('src') - self.height = self.xml.get('height') - self.width = self.xml.get('width') + @classmethod + def get_attributes(cls): + """ + Note: src, height, and width are all required. + """ + return [Attribute('src'), + Attribute('height'), + Attribute('width'),] - # if value is of the form [x,y] then parse it and send along coordinates of previous answer + + def setup(self): + """ + 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. @@ -537,19 +647,10 @@ class ImageInput(InputTypeBase): (self.gx, self.gy) = (0, 0) - def _get_render_context(self): + def _extra_context(self): - 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 + return {'gx': self.gx, + 'gy': self.gy} registry.register(ImageInput) @@ -565,30 +666,18 @@ class Crystallography(InputTypeBase): template = "crystallography.html" tags = ['crystallography'] + @classmethod + def get_attributes(cls): + """ + Note: height, width are required. + """ + return [Attribute('size', None), + Attribute('height'), + Attribute('width'), - def setup(self): - self.height = self.xml.get('height') - self.width = self.xml.get('width') - self.size = self.xml.get('size') - - # if specified, then textline is hidden and id is stored in div of name given by hidden - self.hidden = self.xml.get('hidden', '') - - # Escape answers with quotes, so they don't crash the system! - escapedict = {'"': '"'} - self.value = saxutils.escape(self.value, escapedict) - - 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 + # can probably be removed (textline should prob be always-hidden) + Attribute('hidden', ''), + ] registry.register(Crystallography) @@ -603,29 +692,16 @@ class VseprInput(InputTypeBase): 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 + @classmethod + def get_attributes(cls): + """ + Note: height, width are required. + """ + return [Attribute('height'), + Attribute('width'), + Attribute('molecules'), + Attribute('geometries'), + ] registry.register(VseprInput) @@ -646,17 +722,17 @@ class ChemicalEquationInput(InputTypeBase): template = "chemicalequationinput.html" tags = ['chemicalequationinput'] - def setup(self): - self.size = self.xml.get('size', '20') + @classmethod + def get_attributes(cls): + """ + Can set size of text field. + """ + return [Attribute('size', '20'),] - def _get_render_context(self): - context = { - 'id': self.id, - 'value': self.value, - 'status': self.status, - 'size': self.size, - 'previewer': '/static/js/capa/chemical_equation_preview.js', - } - return context + def _extra_context(self): + """ + TODO (vshnayder): Get rid of this once we have a standard way of requiring js to be loaded. + """ + return {'previewer': '/static/js/capa/chemical_equation_preview.js',} registry.register(ChemicalEquationInput) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index f46e2f753a..2370f59dd2 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -19,7 +19,7 @@
% endif - ${status}

- +
${msg|n}
diff --git a/common/lib/capa/capa/templates/javascriptinput.html b/common/lib/capa/capa/templates/javascriptinput.html index 8b4c8f7115..b4d007e4d8 100644 --- a/common/lib/capa/capa/templates/javascriptinput.html +++ b/common/lib/capa/capa/templates/javascriptinput.html @@ -2,7 +2,7 @@
+ data-submission="${value|h}" data-evaluation="${msg|h}">
diff --git a/common/lib/capa/capa/templates/textline.html b/common/lib/capa/capa/templates/textline.html index 97c512fc00..fbb5467b67 100644 --- a/common/lib/capa/capa/templates/textline.html +++ b/common/lib/capa/capa/templates/textline.html @@ -20,7 +20,7 @@
% endif - % endif - diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index b3d7702246..826d304717 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -2,9 +2,18 @@ Tests of input types. TODO: +- refactor: so much repetive code (have factory methods that build xml elements directly, etc) + +- test error cases + +- check rendering -- e.g. msg should appear in the rendered output. If possible, test that + templates are escaping things properly. + + - 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 lxml import etree @@ -46,6 +55,19 @@ class OptionInputTest(unittest.TestCase): 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): ''' Test choice groups, radio groups, and checkbox groups @@ -73,6 +95,7 @@ class ChoiceGroupTest(unittest.TestCase): expected = {'id': 'sky_input', 'value': 'foil3', 'status': 'answered', + 'msg': '', 'input_type': expected_input_type, 'choices': [('foil1', 'This is foil One.'), ('foil2', 'This is foil Two.'), @@ -119,12 +142,13 @@ class JavascriptInputTest(unittest.TestCase): context = the_input._get_render_context() expected = {'id': 'prob_1_2', + 'status': 'unanswered', + 'msg': '', + 'value': '3', 'params': params, 'display_file': display_file, 'display_class': display_class, - 'problem_state': problem_state, - 'value': '3', - 'evaluation': '',} + 'problem_state': problem_state,} self.assertEqual(context, expected) @@ -204,9 +228,6 @@ class FileSubmissionTest(unittest.TestCase): element = etree.fromstring(xml_str) - escapedict = {'"': '"'} - esc = lambda s: saxutils.escape(s, escapedict) - state = {'value': 'BumbleBee.py', 'status': 'incomplete', 'feedback' : {'message': '3'}, } @@ -220,8 +241,8 @@ class FileSubmissionTest(unittest.TestCase): '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"]')} + 'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]', + 'required_files': '["cookies.py"]'} self.assertEqual(context, expected) @@ -255,14 +276,15 @@ class CodeInputTest(unittest.TestCase): 'status': 'incomplete', 'feedback' : {'message': '3'}, } - the_input = lookup_tag('codeinput')(test_system, element, state) + input_class = lookup_tag('codeinput') + the_input = input_class(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.', + 'msg': input_class.submitted_msg, 'mode': mode, 'linenumbers': linenumbers, 'rows': rows, @@ -311,8 +333,9 @@ class SchematicTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, - 'initial_value': initial_value, 'status': 'unsubmitted', + 'msg': '', + 'initial_value': initial_value, 'width': width, 'height': height, 'parts': parts, @@ -476,6 +499,7 @@ class ChemicalEquationTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': 'H2OYeah', 'status': 'unanswered', + 'msg': '', 'size': size, 'previewer': '/static/js/capa/chemical_equation_preview.js', }