From 6d9fe76dbb637cfca407bde24abe78329459b720 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 23 Oct 2012 17:35:19 -0400 Subject: [PATCH 01/24] wip. adding tests, basic refactor --- common/lib/capa/capa/inputtypes.py | 70 ++++++++----------- common/lib/capa/capa/tests/test_inputtypes.py | 60 +++++++++++----- 2 files changed, 71 insertions(+), 59 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 40fe24434e..55115e66d8 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -195,48 +195,37 @@ class OptionInput(InputTypeBase): template = "optioninput.html" tags = ['optioninput'] + def __init__(self, system, xml, state): + super(OptionInput, self).__init__(system, xml, state) + + # Extract the options... + options = self.xml.get('options') + if not options: + raise Exception( + "[courseware.capa.inputtypes.optioninput] Missing options specification in " + + etree.tostring(self.xml)) + + # 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) - -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: - - The location of the sky - """ - 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 + context = { + 'id': self.id, + 'value': self.value, + 'state': self.status, + 'msg': self.msg, + 'options': self.osetdict, + 'inline': self.xml.get('inline',''), + } + return context register_input_class(OptionInput) @@ -245,7 +234,6 @@ register_input_class(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 diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 9ef642d468..79cd9b6c98 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -27,24 +27,6 @@ class OptionInputTest(unittest.TestCase): ''' Make sure option inputs work ''' - def test_rendering_new(self): - xml = """""" - 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 = """""" @@ -66,3 +48,45 @@ class OptionInputTest(unittest.TestCase): self.assertEqual(context, expected) +class ChoiceGroupTest(unittest.TestCase): + ''' + Test choice groups. + ''' + def test_mult_choice(self): + xml_str = """ + + + This is foil One. + + + This is foil Two. + + + This is foil Three. + + + This is foil Four. + + + This is foil Five. + + + """ + element = etree.fromstring(xml_str) + + state = {'value': 'Down', + 'id': 'sky_input', + 'status': 'answered'} + option_input = inputtypes.OptionInput(system, element, state) + + context = option_input._get_render_context() + + expected = {'value': 'Down', + 'options': [('Up', 'Up'), ('Down', 'Down')], + 'state': 'answered', + 'msg': '', + 'inline': '', + 'id': 'sky_input'} + + self.assertEqual(context, expected) + From 009d6c2e019f9f8344bcb0673d5f3bbd15d98683 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 24 Oct 2012 14:23:02 -0400 Subject: [PATCH 02/24] ChoiceGroup refactor - Make it into a class. - Combine ChoiceGroup, RadioGroup, CheckboxGroup implementation. (All three tags still work--this just unifies the code) - add tests --- common/lib/capa/capa/inputtypes.py | 151 ++++++++---------- common/lib/capa/capa/tests/test_inputtypes.py | 93 ++++++++--- 2 files changed, 134 insertions(+), 110 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 55115e66d8..acff3abf6a 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -97,8 +97,8 @@ class InputTypeBase(object): 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' @@ -234,49 +234,70 @@ register_input_class(OptionInput) # TODO: consolidate choicegroup, radiogroup, checkboxgroup after discussion of # desired semantics. -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 tags should be immediate children " - "of a , 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: + + + + This is foil One. + + + This is foil Two. + + + This is foil Three. + + + """ + template = "choicegroup.html" + tags = ['choicegroup', 'radiogroup', 'checkboxgroup'] + + def __init__(self, system, xml, state): + super(ChoiceGroup, self).__init__(system, xml, state) + + if self.tag == 'choicegroup': + self.suffix = '' + if self.xml.get('type') == "MultipleChoice": + self.element_type = "radio" + elif self.xml.get('type') == "TrueFalse": + # Huh? Why TrueFalse->checkbox? Each input can be true / false separately? + self.element_type = "checkbox" + else: + 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, + 'state': 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. @@ -285,63 +306,25 @@ def extract_choices(element): choices = [] for choice in element: - if not choice.tag == 'choice': + if choice.tag != 'choice': raise Exception("[courseware.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)) return choices + + +register_input_class(ChoiceGroup) -# 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) - - -_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'): ''' diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 79cd9b6c98..833cc396c2 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -35,7 +35,7 @@ class OptionInputTest(unittest.TestCase): state = {'value': 'Down', 'id': 'sky_input', 'status': 'answered'} - option_input = inputtypes.OptionInput(system, element, state) + option_input = inputtypes.get_class_for_tag('optioninput')(system, element, state) context = option_input._get_render_context() @@ -53,40 +53,81 @@ class ChoiceGroupTest(unittest.TestCase): Test choice groups. ''' def test_mult_choice(self): - xml_str = """ - - - This is foil One. - - - This is foil Two. - - - This is foil Three. - - - This is foil Four. - - - This is foil Five. - + xml_template = """ + + This is foil One. + This is foil Two. + This is foil Three. """ + + def check_type(type_str, expected_input_type): + print "checking for type_str='{0}'".format(type_str) + xml_str = xml_template.format(type_str) + + element = etree.fromstring(xml_str) + + state = {'value': 'foil3', + 'id': 'sky_input', + 'status': 'answered'} + + option_input = inputtypes.get_class_for_tag('choicegroup')(system, element, state) + + context = option_input._get_render_context() + + expected = {'id': 'sky_input', + 'value': 'foil3', + 'state': 'answered', + 'input_type': expected_input_type, + 'choices': [('foil1', 'This is foil One.'), + ('foil2', 'This is foil Two.'), + ('foil3', 'This is foil Three.'),], + 'name_array_suffix': '', # what is this for?? + } + + self.assertEqual(context, expected) + + check_type('', 'radio') + check_type('type=""', 'radio') + check_type('type="MultipleChoice"', 'radio') + check_type('type="TrueFalse"', 'checkbox') + # fallback. + check_type('type="StrangeUnknown"', 'radio') + + + def check_group(self, tag, expected_input_type, expected_suffix): + xml_str = """ + <{tag}> + This is foil One. + This is foil Two. + This is foil Three. + + """.format(tag=tag) + element = etree.fromstring(xml_str) - state = {'value': 'Down', + state = {'value': 'foil3', 'id': 'sky_input', 'status': 'answered'} - option_input = inputtypes.OptionInput(system, element, state) - context = option_input._get_render_context() + the_input = inputtypes.get_class_for_tag(tag)(system, element, state) - expected = {'value': 'Down', - 'options': [('Up', 'Up'), ('Down', 'Down')], + context = the_input._get_render_context() + + expected = {'id': 'sky_input', + 'value': 'foil3', 'state': 'answered', - 'msg': '', - 'inline': '', - 'id': 'sky_input'} + 'input_type': expected_input_type, + 'choices': [('foil1', 'This is foil One.'), + ('foil2', 'This is foil Two.'), + ('foil3', 'This is foil Three.'),], + 'name_array_suffix': expected_suffix, # what is this for?? + } self.assertEqual(context, expected) + def test_radiogroup(self): + self.check_group('radiogroup', 'radio', '[]') + + def test_checkboxgroup(self): + self.check_group('checkboxgroup', 'checkbox', '[]') From d0a9b231a49a101dbf6b209979590dbe5023758b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 24 Oct 2012 15:27:45 -0400 Subject: [PATCH 03/24] Turn javascriptinput into a class, add tests --- common/lib/capa/capa/inputtypes.py | 55 +++++++++++-------- common/lib/capa/capa/tests/test_inputtypes.py | 40 +++++++++++++- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index acff3abf6a..43259b3f0b 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -318,7 +318,7 @@ def extract_choices(element): choices.append((choice.get("name"), choice_text)) return choices - + register_input_class(ChoiceGroup) @@ -326,37 +326,44 @@ register_input_class(ChoiceGroup) #----------------------------------------------------------------------------- -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' + template = "javascriptinput.html" + tags = ['javascriptinput'] - 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, + def __init__(self, system, xml, state): + super(JavascriptInput, self).__init__(system, xml, state) + # 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) +register_input_class(JavascriptInput) def textline(element, value, status, render_template, msg=""): diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 833cc396c2..fa3f7d0595 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -8,6 +8,7 @@ from mock import Mock from nose.plugins.skip import SkipTest import os import unittest +import xml.sax.saxutils as saxutils from . import test_system from capa import inputtypes @@ -128,6 +129,43 @@ class ChoiceGroupTest(unittest.TestCase): 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" + display_class = "a_class" + display_file = "my_files/hi.js" + + xml_str = """""".format( + params=params, + ps=saxutils.quoteattr(problem_state)[1:-1], # don't want the outer quotes + dc=display_class, df=display_file) + + element = etree.fromstring(xml_str) + + state = {'value': '3',} + the_input = inputtypes.get_class_for_tag('javascriptinput')(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) From 0c6f6f873b470eec42d13b9fad0e56545c6b197c Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 24 Oct 2012 17:14:57 -0400 Subject: [PATCH 04/24] add a template for a new input type class --- common/lib/capa/capa/inputtypes.py | 32 ++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 43259b3f0b..ed7125859b 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -1,3 +1,29 @@ + + +# template: +''' +class ClassName(InputTypeBase): + """ + """ + + template = "tagname.html" + tags = ['tagname'] + + def __init__(self, system, xml, state): + super(ClassName, self).__init__(system, xml, state) + + + def _get_render_context(self): + + context = {'id': self.id, + + } + return context + +register_input_class(ClassName) +''' + + # # File: courseware/capa/inputtypes.py # @@ -366,6 +392,8 @@ class JavascriptInput(InputTypeBase): register_input_class(JavascriptInput) +#----------------------------------------------------------------------------- + def textline(element, value, status, render_template, msg=""): ''' Simple text line input, with optional size specification. @@ -677,8 +705,8 @@ def imageinput(element, value, status, render_template, msg=''): 'src': src, 'gx': gx, 'gy': gy, - 'state': status, # to change - 'msg': msg, # to change + 'state': status, # to change + 'msg': msg, # to change } html = render_template("imageinput.html", context) return etree.XML(html) From c2682273a81a1713617460ad9f480190e248ced2 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 24 Oct 2012 18:55:13 -0400 Subject: [PATCH 05/24] Convert textline input into class. Unify math and non-math code and templates. Add tests. --- common/lib/capa/capa/inputtypes.py | 130 +++++++----------- common/lib/capa/capa/templates/textinput.html | 43 ++++-- .../capa/templates/textinput_dynamath.html | 50 ------- common/lib/capa/capa/tests/test_inputtypes.py | 57 ++++++++ 4 files changed, 137 insertions(+), 143 deletions(-) delete mode 100644 common/lib/capa/capa/templates/textinput_dynamath.html diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index ed7125859b..82b5e04843 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -16,7 +16,7 @@ class ClassName(InputTypeBase): def _get_render_context(self): context = {'id': self.id, - + } return context @@ -394,92 +394,58 @@ register_input_class(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', '') - raise Exception(msg) +class TextLine(InputTypeBase): + """ - 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) + template = "textinput.html" + tags = ['textline'] - context = {'id': eid, - 'value': value, - 'state': status, - 'count': count, - 'size': size, - 'msg': msg, - 'hidden': hidden, - 'inline': element.get('inline',''), + def __init__(self, system, xml, state): + super(TextLine, self).__init__(system, xml, state) + 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) + + # TODO (vshnayder): can we get rid of inline? Was it one of + # the styling hacks early this semester? + 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, + 'state': 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) - -#----------------------------------------------------------------------------- - - -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 - # 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 `{::}` - 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', '') - - # 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 - - # Escape characters in student input for safe XML parsing - escapedict = {'"': '"'} - value = saxutils.escape(value, 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) - +register_input_class(TextLine) #----------------------------------------------------------------------------- def filesubmission(element, value, status, render_template, msg=''): diff --git a/common/lib/capa/capa/templates/textinput.html b/common/lib/capa/capa/templates/textinput.html index 9b66654117..14d54d4cde 100644 --- a/common/lib/capa/capa/templates/textinput.html +++ b/common/lib/capa/capa/templates/textinput.html @@ -1,6 +1,18 @@ <% doinline = "inline" if inline else "" %> -
+<% +# TODO: +# Is id inputtype_${id} vs textinput_${id} important? +# Is class capa_inputtype vs textinput important? +# should really just use one. +%> +
+ + % if preprocessor is not None: +
+
+ % endif + % if state == 'unsubmitted':
% elif state == 'correct': @@ -15,12 +27,15 @@ % endif

@@ -35,12 +50,18 @@ % endif

-

+

+ + % if do_math: +
`{::}`
+ % endif + +% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +
+% endif % if msg: ${msg|n} % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden: -
-% endif +
diff --git a/common/lib/capa/capa/templates/textinput_dynamath.html b/common/lib/capa/capa/templates/textinput_dynamath.html deleted file mode 100644 index d1de22ef27..0000000000 --- a/common/lib/capa/capa/templates/textinput_dynamath.html +++ /dev/null @@ -1,50 +0,0 @@ -### -### version of textline.html which does dynamic math -### -
- - % if preprocessor is not None: -
-
- % endif - - % if state == 'unsubmitted': -
- % elif state == 'correct': -
- % elif state == 'incorrect': -
- % elif state == 'incomplete': -
- % endif - % if hidden: -
- % endif - - -

- % if state == 'unsubmitted': - unanswered - % elif state == 'correct': - correct - % elif state == 'incorrect': - incorrect - % elif state == 'incomplete': - incomplete - % endif -

- -

- -
`{::}`
- -
- - % if msg: - ${msg|n} - % endif -
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index fa3f7d0595..30a0ded1ee 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -169,3 +169,60 @@ class JavascriptInputTest(unittest.TestCase): '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 = """""".format(size=size) + + element = etree.fromstring(xml_str) + + state = {'value': 'BumbleBee',} + the_input = inputtypes.get_class_for_tag('textline')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'BumbleBee', + 'state': '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 = """""".format(size=size, pp=preprocessorClass, sc=script) + + element = etree.fromstring(xml_str) + + state = {'value': 'BumbleBee',} + the_input = inputtypes.get_class_for_tag('textline')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'BumbleBee', + 'state': 'unanswered', + 'size': size, + 'msg': '', + 'hidden': False, + 'inline': False, + 'do_math': True, + 'preprocessor': {'class_name': preprocessorClass, + 'script_src': script}} + self.assertEqual(context, expected) From c8bc46b6faa6d4b24354e045912257ac19b3a15b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Wed, 24 Oct 2012 19:14:29 -0400 Subject: [PATCH 06/24] Convert FileSubmission input to be a class. - add test --- common/lib/capa/capa/inputtypes.py | 62 +++++++++++-------- common/lib/capa/capa/tests/test_inputtypes.py | 45 +++++++++++++- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 82b5e04843..2eb5bf45bc 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -448,36 +448,44 @@ class TextLine(InputTypeBase): register_input_class(TextLine) #----------------------------------------------------------------------------- -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 = 'Submitted to grader.' +class FileSubmission(InputTypeBase): + """ + Upload some files (e.g. for programming assignments) + """ - 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) + template = "filesubmission.html" + tags = ['filesubmission'] -_reg(filesubmission) + def __init__(self, system, xml, state): + super(FileSubmission, self).__init__(system, xml, state) + escapedict = {'"': '"'} + self.allowed_files = json.dumps(xml.get('allowed_files', '').split()) + self.allowed_files = saxutils.escape(self.allowed_files, escapedict) + self.required_files = json.dumps(xml.get('required_files', '').split()) + self.required_files = saxutils.escape(self.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 self.status == 'incomplete': + self.status = 'queued' + self.queue_len = self.msg + self.msg = 'Submitted to grader.' + + + def _get_render_context(self): + + context = {'id': self.id, + 'state': 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 + +register_input_class(FileSubmission) #----------------------------------------------------------------------------- diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 30a0ded1ee..759ec8bdfa 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -24,6 +24,9 @@ def tst_render_template(template, context): 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 @@ -150,7 +153,7 @@ class JavascriptInputTest(unittest.TestCase): xml_str = """""".format( params=params, - ps=saxutils.quoteattr(problem_state)[1:-1], # don't want the outer quotes + ps=quote_attr(problem_state), dc=display_class, df=display_file) element = etree.fromstring(xml_str) @@ -226,3 +229,43 @@ class TextLineTest(unittest.TestCase): '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 = """""".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'}, } + the_input = inputtypes.get_class_for_tag('filesubmission')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'state': 'queued', + 'msg': 'Submitted to grader.', + 'value': 'BumbleBee.py', + 'queue_len': '3', + 'allowed_files': esc('["runme.py", "nooooo.rb", "ohai.java"]'), + 'required_files': esc('["cookies.py"]')} + + self.assertEqual(context, expected) + From 935e370184e0c99510f3702af060561ff34dcf86 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Thu, 25 Oct 2012 14:56:16 -0400 Subject: [PATCH 07/24] Refactor textbox. - tests - rename it codeinput, with textbox still supported too --- common/lib/capa/capa/inputtypes.py | 99 +++++++++---------- .../{textbox.html => codeinput.html} | 0 common/lib/capa/capa/tests/test_inputtypes.py | 51 +++++++++- 3 files changed, 97 insertions(+), 53 deletions(-) rename common/lib/capa/capa/templates/{textbox.html => codeinput.html} (100%) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 2eb5bf45bc..bbbddc0fb6 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -489,66 +489,61 @@ register_input_class(FileSubmission) #----------------------------------------------------------------------------- -## TODO: Make a wrapper for -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. - ''' - 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', '') +class CodeInput(InputTypeBase): + """ + A text area input for code--uses codemirror, does syntax highlighting, special tab handling, + etc. + """ - # if no student input yet, then use the default input given by the problem - if not value: - value = element.text + template = "codeinput.html" + tags = ['codeinput', + 'textbox', # Old name for this. Still supported, but deprecated. + ] - # 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.' + def __init__(self, system, xml, state): + super(CodeInput, self).__init__(system, xml, state) - # For CodeMirror - mode = element.get('mode','python') - linenumbers = element.get('linenumbers','true') - tabsize = element.get('tabsize','4') - tabsize = int(tabsize) + self.rows = xml.get('rows') or '30' + self.cols = xml.get('cols') or '80' + # if specified, then textline is hidden and id is stored in div of name given by hidden + self.hidden = xml.get('hidden', '') - 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, + # if no student input yet, then use the default input given by the problem + if not self.value: + self.value = xml.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.' + + # For CodeMirror + self.mode = xml.get('mode', 'python') + self.linenumbers = xml.get('linenumbers', 'true') + self.tabsize = int(xml.get('tabsize', '4')) + + def _get_render_context(self): + + context = {'id': self.id, + 'value': self.value, + 'state': 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 += '
Original message: %s' % msg.replace('<', '<') - context['msg'] = newmsg - html = render_template("textbox.html", context) - xhtml = etree.XML(html) - return xhtml + return context +register_input_class(CodeInput) -_reg(textbox) #----------------------------------------------------------------------------- def schematic(element, value, status, render_template, msg=''): diff --git a/common/lib/capa/capa/templates/textbox.html b/common/lib/capa/capa/templates/codeinput.html similarity index 100% rename from common/lib/capa/capa/templates/textbox.html rename to common/lib/capa/capa/templates/codeinput.html diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 759ec8bdfa..8cfbbc54ef 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -251,7 +251,7 @@ class FileSubmissionTest(unittest.TestCase): escapedict = {'"': '"'} esc = lambda s: saxutils.escape(s, escapedict) - + state = {'value': 'BumbleBee.py', 'status': 'incomplete', 'feedback' : {'message': '3'}, } @@ -269,3 +269,52 @@ class FileSubmissionTest(unittest.TestCase): 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 = """""".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 = inputtypes.get_class_for_tag('codeinput')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': 'print "good evening"', + 'state': 'queued', + 'msg': 'Submitted to grader.', + 'mode': mode, + 'linenumbers': linenumbers, + 'rows': rows, + 'cols': cols, + 'hidden': '', + 'tabsize': int(tabsize), + 'queue_len': '3', + } + + self.assertEqual(context, expected) + From 80d0952d161a6ef4e795336bd8109e155807ed46 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 26 Oct 2012 18:29:46 -0400 Subject: [PATCH 08/24] Refactor schematic input --- common/lib/capa/capa/inputtypes.py | 52 ++++++++++-------- common/lib/capa/capa/tests/test_inputtypes.py | 55 ++++++++++++++++++- 2 files changed, 84 insertions(+), 23 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index bbbddc0fb6..cbd5d1d02e 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -546,29 +546,37 @@ register_input_class(CodeInput) #----------------------------------------------------------------------------- -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'] + + def __init__(self, system, xml, state): + super(Schematic, self).__init__(system, xml, state) + self.height = xml.get('height') + self.width = xml.get('width') + self.parts = xml.get('parts') + self.analyses = xml.get('analyses') + self.initial_value = xml.get('initial_value') + self.submit_analyses = xml.get('submit_analyses') + + + def _get_render_context(self): + + context = {'id': self.id, + 'value': self.value, + 'initial_value': self.initial_value, + 'state': self.status, + 'width': self.width, + 'height': self.height, + 'parts': self.parts, + 'analyses': self.analyses, + 'submit_analyses': self.submit_analyses, } + return context + +register_input_class(Schematic) #----------------------------------------------------------------------------- ### TODO: Move out of inputtypes diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 8cfbbc54ef..573f52a01a 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -1,5 +1,9 @@ """ -Tests of input types (and actually responsetypes too) +Tests of input types (and actually responsetypes too). + +TODO: +- test unicode in values, parameters, etc. +- test various html escapes """ from datetime import datetime @@ -318,3 +322,52 @@ class CodeInputTest(unittest.TestCase): 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 = """""".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', + 'feedback' : {'message': '3'}, } + + the_input = inputtypes.get_class_for_tag('schematic')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': value, + 'initial_value': initial_value, + 'state': 'unsubmitted', + 'width': width, + 'height': height, + 'parts': parts, + 'analyses': analyses, + 'submit_analyses': submit_analyses, + } + + self.assertEqual(context, expected) + From 9135d7b2db316b6ad31857ea9d88310ffd83666e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 26 Oct 2012 19:05:56 -0400 Subject: [PATCH 09/24] minor cleanup in math function --- common/lib/capa/capa/inputtypes.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index cbd5d1d02e..5d271f257e 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -596,18 +596,14 @@ def math(element, value, status, render_template, msg=''): ''' mathstr = re.sub('\$(.*)\$', '[mathjaxinline]\\1[/mathjaxinline]', element.text) mtag = 'mathjax' - if not '\\displaystyle' in mathstr: mtag += 'inline' - else: mathstr = mathstr.replace('\\displaystyle', '') + 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}) + # TODO: why are there nested html tags here?? Why are there html tags at all, in fact? html = '%s%s' % (mathstr, saxutils.escape(element.tail)) try: xhtml = etree.XML(html) From 2fab97c1660a65cc741b296f4a028067e28f71e5 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Fri, 26 Oct 2012 19:06:34 -0400 Subject: [PATCH 10/24] Refactor imageinput and crystallography. - also add a test for chemicalequationinput --- common/lib/capa/capa/inputtypes.py | 203 +++++++++--------- .../capa/capa/templates/crystallography.html | 18 +- .../lib/capa/capa/templates/vsepr_input.html | 24 +-- common/lib/capa/capa/tests/test_inputtypes.py | 121 ++++++++++- 4 files changed, 231 insertions(+), 135 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 5d271f257e..145eabd953 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -647,9 +647,8 @@ _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. @@ -657,130 +656,120 @@ 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 __init__(self, system, xml, state): + super(ImageInput, self).__init__(system, xml, state) + self.src = xml.get('src') + self.height = xml.get('height') + self.width = 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: + # TODO (vshnayder): why is there a "-15" here?? + (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', '') - 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, + 'state': self.status, # to change (VS: to what??) + 'msg': self.msg, # to change } + return context - html = render_template("crystallography.html", context) +register_input_class(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? + """ -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', '') - raise Exception(msg) - height = element.get('height') - width = element.get('width') - display_file = element.get('display_file') + template = "crystallography.html" + tags = ['crystallography'] - 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) + def __init__(self, system, xml, state): + super(Crystallography, self).__init__(system, xml, state) - molecules = element.get('molecules') - geometries = element.get('geometries') + self.height = xml.get('height') + self.width = xml.get('width') + self.size = xml.get('size') - 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, + # if specified, then textline is hidden and id is stored in div of name given by hidden + self.hidden = 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, + 'state': 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) +register_input_class(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 __init__(self, system, xml, state): + super(ImageInput, self).__init__(system, xml, state) + + self.height = xml.get('height') + self.width = xml.get('width') + + # Escape answers with quotes, so they don't crash the system! + escapedict = {'"': '"'} + self.value = saxutils.escape(self.value, escapedict) + + self.molecules = xml.get('molecules') + self.geometries = xml.get('geometries') + + def _get_render_context(self): + + context = {'id': self.id, + 'value': self.value, + 'state': self.status, + 'msg': self.msg, + 'width': self.width, + 'height': self.height, + 'molecules': self.molecules, + 'geometries': self.geometries, + } + return context + +register_input_class(VseprInput) #-------------------------------------------------------------------------------- diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 1fc638b356..71578f1fa0 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -1,19 +1,19 @@ -<% doinline = "inline" if inline else "" %> - -
+
-
+
+
+
% if state == 'unsubmitted': -
+
% elif state == 'correct': -
+
% elif state == 'incorrect': -
+
% elif state == 'incomplete': -
+
% endif % if hidden:
@@ -45,7 +45,7 @@ % if msg: ${msg|n} % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden: +% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% endif
diff --git a/common/lib/capa/capa/templates/vsepr_input.html b/common/lib/capa/capa/templates/vsepr_input.html index 588e53c914..5194551d50 100644 --- a/common/lib/capa/capa/templates/vsepr_input.html +++ b/common/lib/capa/capa/templates/vsepr_input.html @@ -1,6 +1,4 @@ -<% doinline = "inline" if inline else "" %> - -
+
@@ -14,25 +12,17 @@
% if state == 'unsubmitted': -
+
% elif state == 'correct': -
+
% elif state == 'incorrect': -
+
% elif state == 'incomplete': -
- % endif - % if hidden: -
+
% endif

@@ -52,7 +42,7 @@ % if msg: ${msg|n} % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden: +% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:

% endif diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 573f52a01a..773f2b3519 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -4,6 +4,7 @@ Tests of input types (and actually responsetypes too). 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 @@ -351,8 +352,7 @@ class SchematicTest(unittest.TestCase): value = 'three resistors and an oscilating pendulum' state = {'value': value, - 'status': 'unsubmitted', - 'feedback' : {'message': '3'}, } + 'status': 'unsubmitted'} the_input = inputtypes.get_class_for_tag('schematic')(system, element, state) @@ -371,3 +371,120 @@ class SchematicTest(unittest.TestCase): 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 = """""".format(s=src, h=height, w=width) + + element = etree.fromstring(xml_str) + + state = {'value': value, + 'status': 'unsubmitted'} + + the_input = inputtypes.get_class_for_tag('imageinput')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': value, + 'state': 'unsubmitted', + 'width': width, + 'height': height, + 'src': src, + 'gx': egx, + 'gy': egy, + 'state': 'unsubmitted', + 'msg': ''} + + self.assertEqual(context, expected) + + def test_with_value(self): + 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 = """""".format(h=height, w=width, s=size) + + element = etree.fromstring(xml_str) + + value = 'abc' + state = {'value': value, + 'status': 'unsubmitted'} + + the_input = inputtypes.get_class_for_tag('crystallography')(system, element, state) + + context = the_input._get_render_context() + + expected = {'id': 'prob_1_2', + 'value': value, + 'state': 'unsubmitted', + 'size': size, + 'msg': '', + 'hidden': '', + 'width': width, + 'height': height, + } + + self.assertEqual(context, expected) + + +class ChemicalEquationTest(unittest.TestCase): + ''' + Check that chemical equation inputs work. + ''' + + def test_rendering(self): + size = "42" + xml_str = """""".format(size=size) + + element = etree.fromstring(xml_str) + + state = {'value': 'H2OYeah',} + the_input = inputtypes.get_class_for_tag('chemicalequationinput')(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) + From f6637b7fe03e9d72b8053972c444840921b84d6e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 27 Oct 2012 19:29:24 -0400 Subject: [PATCH 11/24] Add a registry that keeps track of tag->renderer/input mappings --- common/lib/capa/capa/registry.py | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 common/lib/capa/capa/registry.py diff --git a/common/lib/capa/capa/registry.py b/common/lib/capa/capa/registry.py new file mode 100644 index 0000000000..94a2853dec --- /dev/null +++ b/common/lib/capa/capa/registry.py @@ -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] From d36d8cba6b1ce624f8202c366b20ed1b113f0dcf Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 27 Oct 2012 19:31:17 -0400 Subject: [PATCH 12/24] Cleanups: - switch to using registry - pull math and solution "input types" into a separate customrender.py file - make capa_problem use new custom renderers and input types registries - remove unused imports, methods, etc - add tests for math and solution tags. --- common/lib/capa/capa/capa_problem.py | 37 ++- common/lib/capa/capa/customrender.py | 100 +++++++++ common/lib/capa/capa/inputtypes.py | 210 +++--------------- common/lib/capa/capa/tests/__init__.py | 12 +- .../lib/capa/capa/tests/test_customrender.py | 76 +++++++ common/lib/capa/capa/tests/test_inputtypes.py | 43 ++-- 6 files changed, 243 insertions(+), 235 deletions(-) create mode 100644 common/lib/capa/capa/customrender.py create mode 100644 common/lib/capa/capa/tests/test_customrender.py diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 1c31725e4b..451891d067 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 ... 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) diff --git a/common/lib/capa/capa/customrender.py b/common/lib/capa/capa/customrender.py new file mode 100644 index 0000000000..ef1044e8b1 --- /dev/null +++ b/common/lib/capa/capa/customrender.py @@ -0,0 +1,100 @@ +""" +This has custom renderers: classes that know how to render certain problem tags (e.g. and +) 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: + + $\displaystyle U(r)=4 U_0 $ + $r_0$ + + 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 = '%s%s' % ( + self.mathstr, saxutils.escape(self.xml.tail)) + try: + xhtml = etree.XML(html) + except Exception as err: + if self.system.DEBUG: + msg = '

Error %s

' % ( + str(err).replace('<', '<')) + msg += ('

Failed to construct math expression from

%s

' % + html.replace('<', '<')) + msg += "
" + log.error(msg) + return etree.XML(msg) + else: + raise + return xhtml + + +registry.register(MathRenderer) + +#----------------------------------------------------------------------------- + +class SolutionRenderer(object): + ''' + A solution is just a ... 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) + diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 145eabd953..9569958c96 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -1,29 +1,3 @@ - - -# template: -''' -class ClassName(InputTypeBase): - """ - """ - - template = "tagname.html" - tags = ['tagname'] - - def __init__(self, system, xml, state): - super(ClassName, self).__init__(system, xml, state) - - - def _get_render_context(self): - - context = {'id': self.id, - - } - return context - -register_input_class(ClassName) -''' - - # # File: courseware/capa/inputtypes.py # @@ -32,11 +6,9 @@ register_input_class(ClassName) 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) @@ -60,53 +32,13 @@ import json from lxml import etree 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): """ @@ -119,16 +51,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' -- 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}" + * '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 @@ -172,40 +106,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)) - - #----------------------------------------------------------------------------- @@ -253,7 +160,7 @@ class OptionInput(InputTypeBase): } return context -register_input_class(OptionInput) +registry.register(OptionInput) #----------------------------------------------------------------------------- @@ -346,7 +253,7 @@ def extract_choices(element): return choices -register_input_class(ChoiceGroup) +registry.register(ChoiceGroup) #----------------------------------------------------------------------------- @@ -389,7 +296,7 @@ class JavascriptInput(InputTypeBase): } return context -register_input_class(JavascriptInput) +registry.register(JavascriptInput) #----------------------------------------------------------------------------- @@ -445,7 +352,7 @@ class TextLine(InputTypeBase): } return context -register_input_class(TextLine) +registry.register(TextLine) #----------------------------------------------------------------------------- @@ -485,7 +392,7 @@ class FileSubmission(InputTypeBase): 'required_files': self.required_files,} return context -register_input_class(FileSubmission) +registry.register(FileSubmission) #----------------------------------------------------------------------------- @@ -542,7 +449,7 @@ class CodeInput(InputTypeBase): } return context -register_input_class(CodeInput) +registry.register(CodeInput) #----------------------------------------------------------------------------- @@ -576,74 +483,7 @@ class Schematic(InputTypeBase): 'submit_analyses': self.submit_analyses, } return context -register_input_class(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. - - Examples: - - $\displaystyle U(r)=4 U_0 - $r_0$ - - We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline] - - 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) - - - # TODO: why are there nested html tags here?? Why are there html tags at all, in fact? - html = '%s%s' % (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 = '

Error %s

' % str(err).replace('<', '<') - msg += ('

Failed to construct math expression from

%s

' % - html.replace('<', '<')) - msg += "
" - log.error(msg) - return etree.XML(msg) - else: - raise - # xhtml.tail = element.tail # don't forget to include the tail! - return xhtml - -_reg(math) - -#----------------------------------------------------------------------------- - - -def solution(element, value, status, render_template, msg=''): - ''' - This is not really an input type. It is just a ... 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) +registry.register(Schematic) #----------------------------------------------------------------------------- @@ -690,7 +530,7 @@ class ImageInput(InputTypeBase): } return context -register_input_class(ImageInput) +registry.register(ImageInput) #----------------------------------------------------------------------------- @@ -730,7 +570,7 @@ class Crystallography(InputTypeBase): } return context -register_input_class(Crystallography) +registry.register(Crystallography) # ------------------------------------------------------------------------- @@ -799,4 +639,4 @@ class ChemicalEquationInput(InputTypeBase): } return context -register_input_class(ChemicalEquationInput) +registry.register(ChemicalEquationInput) diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py index c72d2a1538..b06975f6ce 100644 --- a/common/lib/capa/capa/tests/__init__.py +++ b/common/lib/capa/capa/tests/__init__.py @@ -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
+ """ + return '
{0}
'.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")), diff --git a/common/lib/capa/capa/tests/test_customrender.py b/common/lib/capa/capa/tests/test_customrender.py new file mode 100644 index 0000000000..7208ab2941 --- /dev/null +++ b/common/lib/capa/capa/tests/test_customrender.py @@ -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 = """{s}""".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 = """{tex}""".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. + diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 773f2b3519..992af14bf9 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -1,5 +1,5 @@ """ -Tests of input types (and actually responsetypes too). +Tests of input types. TODO: - test unicode in values, parameters, etc. @@ -7,27 +7,16 @@ TODO: - test funny xml chars -- should never get xml parse error if things are escaped properly. """ -from datetime import datetime -import json -from mock import Mock -from nose.plugins.skip import SkipTest -import os +from lxml import etree import unittest import xml.sax.saxutils as saxutils from . import test_system from capa import inputtypes -from lxml import etree +# just a handy shortcut +lookup_tag = inputtypes.registry.get_class_for_tag -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) - - -system = Mock(render_template=tst_render_template) def quote_attr(s): return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes @@ -44,7 +33,7 @@ class OptionInputTest(unittest.TestCase): state = {'value': 'Down', 'id': 'sky_input', 'status': 'answered'} - option_input = inputtypes.get_class_for_tag('optioninput')(system, element, state) + option_input = lookup_tag('optioninput')(test_system, element, state) context = option_input._get_render_context() @@ -80,7 +69,7 @@ class ChoiceGroupTest(unittest.TestCase): 'id': 'sky_input', 'status': 'answered'} - option_input = inputtypes.get_class_for_tag('choicegroup')(system, element, state) + option_input = lookup_tag('choicegroup')(test_system, element, state) context = option_input._get_render_context() @@ -119,7 +108,7 @@ class ChoiceGroupTest(unittest.TestCase): 'id': 'sky_input', 'status': 'answered'} - the_input = inputtypes.get_class_for_tag(tag)(system, element, state) + the_input = lookup_tag(tag)(test_system, element, state) context = the_input._get_render_context() @@ -164,7 +153,7 @@ class JavascriptInputTest(unittest.TestCase): element = etree.fromstring(xml_str) state = {'value': '3',} - the_input = inputtypes.get_class_for_tag('javascriptinput')(system, element, state) + the_input = lookup_tag('javascriptinput')(test_system, element, state) context = the_input._get_render_context() @@ -191,7 +180,7 @@ class TextLineTest(unittest.TestCase): element = etree.fromstring(xml_str) state = {'value': 'BumbleBee',} - the_input = inputtypes.get_class_for_tag('textline')(system, element, state) + the_input = lookup_tag('textline')(test_system, element, state) context = the_input._get_render_context() @@ -219,7 +208,7 @@ class TextLineTest(unittest.TestCase): element = etree.fromstring(xml_str) state = {'value': 'BumbleBee',} - the_input = inputtypes.get_class_for_tag('textline')(system, element, state) + the_input = lookup_tag('textline')(test_system, element, state) context = the_input._get_render_context() @@ -260,7 +249,7 @@ class FileSubmissionTest(unittest.TestCase): state = {'value': 'BumbleBee.py', 'status': 'incomplete', 'feedback' : {'message': '3'}, } - the_input = inputtypes.get_class_for_tag('filesubmission')(system, element, state) + the_input = lookup_tag('filesubmission')(test_system, element, state) context = the_input._get_render_context() @@ -304,7 +293,7 @@ class CodeInputTest(unittest.TestCase): 'status': 'incomplete', 'feedback' : {'message': '3'}, } - the_input = inputtypes.get_class_for_tag('codeinput')(system, element, state) + the_input = lookup_tag('codeinput')(test_system, element, state) context = the_input._get_render_context() @@ -354,7 +343,7 @@ class SchematicTest(unittest.TestCase): state = {'value': value, 'status': 'unsubmitted'} - the_input = inputtypes.get_class_for_tag('schematic')(system, element, state) + the_input = lookup_tag('schematic')(test_system, element, state) context = the_input._get_render_context() @@ -393,7 +382,7 @@ class ImageInputTest(unittest.TestCase): state = {'value': value, 'status': 'unsubmitted'} - the_input = inputtypes.get_class_for_tag('imageinput')(system, element, state) + the_input = lookup_tag('imageinput')(test_system, element, state) context = the_input._get_render_context() @@ -447,7 +436,7 @@ class CrystallographyTest(unittest.TestCase): state = {'value': value, 'status': 'unsubmitted'} - the_input = inputtypes.get_class_for_tag('crystallography')(system, element, state) + the_input = lookup_tag('crystallography')(test_system, element, state) context = the_input._get_render_context() @@ -476,7 +465,7 @@ class ChemicalEquationTest(unittest.TestCase): element = etree.fromstring(xml_str) state = {'value': 'H2OYeah',} - the_input = inputtypes.get_class_for_tag('chemicalequationinput')(system, element, state) + the_input = lookup_tag('chemicalequationinput')(test_system, element, state) context = the_input._get_render_context() From cc8f0a16342e009ae2a545fa7021ddaea90df3d4 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 27 Oct 2012 19:57:29 -0400 Subject: [PATCH 13/24] Fix merge bugs --- common/lib/capa/capa/inputtypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 9569958c96..33e9048131 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -584,7 +584,7 @@ class VseprInput(InputTypeBase): tags = ['vsepr_input'] def __init__(self, system, xml, state): - super(ImageInput, self).__init__(system, xml, state) + super(VseprInput, self).__init__(system, xml, state) self.height = xml.get('height') self.width = xml.get('width') @@ -609,7 +609,7 @@ class VseprInput(InputTypeBase): } return context -register_input_class(VseprInput) +registry.register(VseprInput) #-------------------------------------------------------------------------------- From 6324eeeecbf872f39af1c2c8aa672a9a2f7a13e2 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 27 Oct 2012 20:00:18 -0400 Subject: [PATCH 14/24] add note about mysterious -15 in the code --- common/lib/capa/capa/inputtypes.py | 3 ++- common/lib/capa/capa/tests/test_inputtypes.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 33e9048131..0bbf54d153 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -510,7 +510,8 @@ class ImageInput(InputTypeBase): # 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: - # TODO (vshnayder): why is there a "-15" here?? + # Note: we subtract 15 to compensate for the size of the dot on the screen. + # (which supposedly has size 30). (self.gx, self.gy) = [int(x) - 15 for x in m.groups()] else: (self.gx, self.gy) = (0, 0) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 992af14bf9..ba20f84cb1 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -400,6 +400,7 @@ class ImageInputTest(unittest.TestCase): 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): From 1d4990458e5248565f68e59ede55409c2b5a6f0a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Sat, 27 Oct 2012 20:09:02 -0400 Subject: [PATCH 15/24] add todos about would-be-nice refactors --- common/lib/capa/capa/inputtypes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 0bbf54d153..08cd9ce413 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -25,6 +25,13 @@ graded status as'status' # 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. + import logging import re import shlex # for splitting quoted strings From 1a843c2d77ba61664d2b913b55785806045c9e4e Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:23:27 -0400 Subject: [PATCH 16/24] Remove non-working TrueFalse functionality from choicegroup, combine tests with radiogroup and checkboxgroup --- common/lib/capa/capa/inputtypes.py | 11 ++--- common/lib/capa/capa/tests/test_inputtypes.py | 47 ++----------------- 2 files changed, 7 insertions(+), 51 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 08cd9ce413..690ece4dc5 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -202,16 +202,11 @@ class ChoiceGroup(InputTypeBase): def __init__(self, system, xml, state): super(ChoiceGroup, self).__init__(system, xml, state) + # suffix is '' or [] to change the way the input is handled in --as a scalar or vector + # value. (VS: would be nice to make to this less hackish). if self.tag == 'choicegroup': self.suffix = '' - if self.xml.get('type') == "MultipleChoice": - self.element_type = "radio" - elif self.xml.get('type') == "TrueFalse": - # Huh? Why TrueFalse->checkbox? Each input can be true / false separately? - self.element_type = "checkbox" - else: - self.element_type = "radio" - + self.element_type = "radio" elif self.tag == 'radiogroup': self.element_type = "radio" self.suffix = '[]' diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index ba20f84cb1..6c5b1cca15 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -48,50 +48,8 @@ class OptionInputTest(unittest.TestCase): class ChoiceGroupTest(unittest.TestCase): ''' - Test choice groups. + Test choice groups, radio groups, and checkbox groups ''' - def test_mult_choice(self): - xml_template = """ - - This is foil One. - This is foil Two. - This is foil Three. - - """ - - def check_type(type_str, expected_input_type): - print "checking for type_str='{0}'".format(type_str) - xml_str = xml_template.format(type_str) - - element = etree.fromstring(xml_str) - - state = {'value': 'foil3', - 'id': 'sky_input', - 'status': 'answered'} - - option_input = lookup_tag('choicegroup')(test_system, element, state) - - context = option_input._get_render_context() - - expected = {'id': 'sky_input', - 'value': 'foil3', - 'state': 'answered', - 'input_type': expected_input_type, - 'choices': [('foil1', 'This is foil One.'), - ('foil2', 'This is foil Two.'), - ('foil3', 'This is foil Three.'),], - 'name_array_suffix': '', # what is this for?? - } - - self.assertEqual(context, expected) - - check_type('', 'radio') - check_type('type=""', 'radio') - check_type('type="MultipleChoice"', 'radio') - check_type('type="TrueFalse"', 'checkbox') - # fallback. - check_type('type="StrangeUnknown"', 'radio') - def check_group(self, tag, expected_input_type, expected_suffix): xml_str = """ @@ -124,6 +82,9 @@ class ChoiceGroupTest(unittest.TestCase): self.assertEqual(context, expected) + def test_choicegroup(self): + self.check_group('choicegroup', 'radio', '') + def test_radiogroup(self): self.check_group('radiogroup', 'radio', '[]') From d6cb432842e26e56ad3d085ceb1788c9ea09f256 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:42:11 -0400 Subject: [PATCH 17/24] Replace overriding constructor with a setup() method. - allows catching any exceptions and making sure the xml is in the error message - isolates subclasses from external interface a bit --- common/lib/capa/capa/inputtypes.py | 130 +++++++++++++++-------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 690ece4dc5..812c9a6555 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -32,13 +32,14 @@ graded status as'status' # TODO: Quoting and unquoting is handled in a pretty ad-hoc way. Also something that could be done # properly once in InputTypeBase. +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__) @@ -99,6 +100,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 @@ -135,15 +156,11 @@ class OptionInput(InputTypeBase): template = "optioninput.html" tags = ['optioninput'] - def __init__(self, system, xml, state): - super(OptionInput, self).__init__(system, xml, state) - + def setup(self): # Extract the options... options = self.xml.get('options') if not options: - raise Exception( - "[courseware.capa.inputtypes.optioninput] Missing options specification in " - + etree.tostring(self.xml)) + raise ValueError("optioninput: Missing 'options' specification.") # parse the set of possible options oset = shlex.shlex(options[1:-1]) @@ -199,9 +216,7 @@ class ChoiceGroup(InputTypeBase): template = "choicegroup.html" tags = ['choicegroup', 'radiogroup', 'checkboxgroup'] - def __init__(self, system, xml, state): - super(ChoiceGroup, self).__init__(system, xml, state) - + 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 to this less hackish). if self.tag == 'choicegroup': @@ -242,9 +257,9 @@ def extract_choices(element): for choice in element: if choice.tag != 'choice': - raise Exception("[courseware.capa.inputtypes.extract_choices] \ - Expected a tag; got %s instead" - % choice.tag) + 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? @@ -270,8 +285,7 @@ class JavascriptInput(InputTypeBase): template = "javascriptinput.html" tags = ['javascriptinput'] - def __init__(self, system, xml, state): - super(JavascriptInput, self).__init__(system, xml, state) + def setup(self): # Need to provide a value that JSON can parse if there is no # student-supplied value yet. if self.value == "": @@ -311,8 +325,7 @@ class TextLine(InputTypeBase): template = "textinput.html" tags = ['textline'] - def __init__(self, system, xml, state): - super(TextLine, self).__init__(system, xml, state) + def setup(self): self.size = self.xml.get('size') # if specified, then textline is hidden and input id is stored @@ -366,12 +379,11 @@ class FileSubmission(InputTypeBase): template = "filesubmission.html" tags = ['filesubmission'] - def __init__(self, system, xml, state): - super(FileSubmission, self).__init__(system, xml, state) + def setup(self): escapedict = {'"': '"'} - self.allowed_files = json.dumps(xml.get('allowed_files', '').split()) + 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(xml.get('required_files', '').split()) + self.required_files = json.dumps(self.xml.get('required_files', '').split()) self.required_files = saxutils.escape(self.required_files, escapedict) # Check if problem has been queued @@ -410,17 +422,16 @@ class CodeInput(InputTypeBase): 'textbox', # Old name for this. Still supported, but deprecated. ] - def __init__(self, system, xml, state): - super(CodeInput, self).__init__(system, xml, state) - self.rows = xml.get('rows') or '30' - self.cols = xml.get('cols') or '80' + 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 = xml.get('hidden', '') + self.hidden = self.xml.get('hidden', '') # if no student input yet, then use the default input given by the problem if not self.value: - self.value = xml.text + self.value = self.xml.text # Check if problem has been queued self.queue_len = 0 @@ -431,9 +442,9 @@ class CodeInput(InputTypeBase): self.msg = 'Submitted to grader.' # For CodeMirror - self.mode = xml.get('mode', 'python') - self.linenumbers = xml.get('linenumbers', 'true') - self.tabsize = int(xml.get('tabsize', '4')) + 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): @@ -462,14 +473,13 @@ class Schematic(InputTypeBase): template = "schematicinput.html" tags = ['schematic'] - def __init__(self, system, xml, state): - super(Schematic, self).__init__(system, xml, state) - self.height = xml.get('height') - self.width = xml.get('width') - self.parts = xml.get('parts') - self.analyses = xml.get('analyses') - self.initial_value = xml.get('initial_value') - self.submit_analyses = xml.get('submit_analyses') + 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') def _get_render_context(self): @@ -482,7 +492,7 @@ class Schematic(InputTypeBase): 'height': self.height, 'parts': self.parts, 'analyses': self.analyses, - 'submit_analyses': self.submit_analyses, } + 'submit_analyses': self.submit_analyses,} return context registry.register(Schematic) @@ -503,11 +513,10 @@ class ImageInput(InputTypeBase): template = "imageinput.html" tags = ['imageinput'] - def __init__(self, system, xml, state): - super(ImageInput, self).__init__(system, xml, state) - self.src = xml.get('src') - self.height = xml.get('height') - self.width = xml.get('width') + def setup(self): + self.src = self.xml.get('src') + self.height = self.xml.get('height') + self.width = self.xml.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]+)]', self.value.strip().replace(' ', '')) @@ -547,15 +556,14 @@ class Crystallography(InputTypeBase): template = "crystallography.html" tags = ['crystallography'] - def __init__(self, system, xml, state): - super(Crystallography, self).__init__(system, xml, state) - self.height = xml.get('height') - self.width = xml.get('width') - self.size = xml.get('size') + 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 = xml.get('hidden', '') + self.hidden = self.xml.get('hidden', '') # Escape answers with quotes, so they don't crash the system! escapedict = {'"': '"'} @@ -586,18 +594,16 @@ class VseprInput(InputTypeBase): template = 'vsepr_input.html' tags = ['vsepr_input'] - def __init__(self, system, xml, state): - super(VseprInput, self).__init__(system, xml, state) - - self.height = xml.get('height') - self.width = xml.get('width') + 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 = xml.get('molecules') - self.geometries = xml.get('geometries') + self.molecules = self.xml.get('molecules') + self.geometries = self.xml.get('geometries') def _get_render_context(self): @@ -631,13 +637,15 @@ 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 From a97298e8f7c1dd85fd49af191d21d5ada3fc5488 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:48:53 -0400 Subject: [PATCH 18/24] add note about documenting js input --- common/lib/capa/capa/inputtypes.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 812c9a6555..e295ac9013 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -280,6 +280,11 @@ 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. + + 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. """ template = "javascriptinput.html" From 02bd99697b9907da3ddf860bb25618a074fbc98b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:49:33 -0400 Subject: [PATCH 19/24] Clean up section ids and classes - all inputtypes should have id inputtype_${id} - should have class capa_inputtype --- common/lib/capa/capa/templates/crystallography.html | 2 +- common/lib/capa/capa/templates/textinput.html | 8 +------- common/lib/capa/capa/templates/vsepr_input.html | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 71578f1fa0..ee9b249d3d 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -1,4 +1,4 @@ -
+
diff --git a/common/lib/capa/capa/templates/textinput.html b/common/lib/capa/capa/templates/textinput.html index 14d54d4cde..d2059fdc58 100644 --- a/common/lib/capa/capa/templates/textinput.html +++ b/common/lib/capa/capa/templates/textinput.html @@ -1,12 +1,6 @@ <% doinline = "inline" if inline else "" %> -<% -# TODO: -# Is id inputtype_${id} vs textinput_${id} important? -# Is class capa_inputtype vs textinput important? -# should really just use one. -%> -
+
% if preprocessor is not None:
diff --git a/common/lib/capa/capa/templates/vsepr_input.html b/common/lib/capa/capa/templates/vsepr_input.html index 5194551d50..a6bd465554 100644 --- a/common/lib/capa/capa/templates/vsepr_input.html +++ b/common/lib/capa/capa/templates/vsepr_input.html @@ -1,4 +1,4 @@ -
+
From 318ea9b1353fccfefbeec1eda646d2927527c16a Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:51:58 -0400 Subject: [PATCH 20/24] add back mathjax textarea --- common/lib/capa/capa/templates/textinput.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/lib/capa/capa/templates/textinput.html b/common/lib/capa/capa/templates/textinput.html index d2059fdc58..3685742a3c 100644 --- a/common/lib/capa/capa/templates/textinput.html +++ b/common/lib/capa/capa/templates/textinput.html @@ -48,6 +48,9 @@ % if do_math:
`{::}`
+ + % endif % if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: From ab03f3dddf4e32f7200c2951b7a2349aca771d9b Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 07:56:10 -0400 Subject: [PATCH 21/24] move textinput.html to textline.html for consistency with other inputs --- common/lib/capa/capa/inputtypes.py | 2 +- .../lib/capa/capa/templates/{textinput.html => textline.html} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename common/lib/capa/capa/templates/{textinput.html => textline.html} (100%) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index e295ac9013..26e2de8fa4 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -327,7 +327,7 @@ class TextLine(InputTypeBase): """ - template = "textinput.html" + template = "textline.html" tags = ['textline'] def setup(self): diff --git a/common/lib/capa/capa/templates/textinput.html b/common/lib/capa/capa/templates/textline.html similarity index 100% rename from common/lib/capa/capa/templates/textinput.html rename to common/lib/capa/capa/templates/textline.html From 516b720f1e393a04443e37fea844d53134a8697c Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 08:05:07 -0400 Subject: [PATCH 22/24] Rename: use 'status' in templates as well as in the code. --- common/lib/capa/capa/inputtypes.py | 22 ++++++++----------- .../lib/capa/capa/templates/choicegroup.html | 8 +++---- common/lib/capa/capa/templates/codeinput.html | 10 ++++----- .../capa/capa/templates/crystallography.html | 18 +++++++-------- .../capa/capa/templates/filesubmission.html | 10 ++++----- .../lib/capa/capa/templates/imageinput.html | 8 +++---- .../lib/capa/capa/templates/jstextline.html | 8 +++---- .../lib/capa/capa/templates/optioninput.html | 8 +++---- .../capa/capa/templates/schematicinput.html | 8 +++---- common/lib/capa/capa/templates/textline.html | 18 +++++++-------- .../lib/capa/capa/templates/vsepr_input.html | 18 +++++++-------- common/lib/capa/capa/tests/test_inputtypes.py | 19 ++++++++-------- 12 files changed, 75 insertions(+), 80 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 26e2de8fa4..e2d7708ee3 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -21,10 +21,6 @@ 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. @@ -177,7 +173,7 @@ class OptionInput(InputTypeBase): context = { 'id': self.id, 'value': self.value, - 'state': self.status, + 'status': self.status, 'msg': self.msg, 'options': self.osetdict, 'inline': self.xml.get('inline',''), @@ -236,7 +232,7 @@ class ChoiceGroup(InputTypeBase): def _get_render_context(self): context = {'id': self.id, 'value': self.value, - 'state': self.status, + 'status': self.status, 'input_type': self.element_type, 'choices': self.choices, 'name_array_suffix': self.suffix} @@ -362,7 +358,7 @@ class TextLine(InputTypeBase): context = {'id': self.id, 'value': value, - 'state': self.status, + 'status': self.status, 'size': self.size, 'msg': self.msg, 'hidden': self.hidden, @@ -403,7 +399,7 @@ class FileSubmission(InputTypeBase): def _get_render_context(self): context = {'id': self.id, - 'state': self.status, + 'status': self.status, 'msg': self.msg, 'value': self.value, 'queue_len': self.queue_len, @@ -455,7 +451,7 @@ class CodeInput(InputTypeBase): context = {'id': self.id, 'value': self.value, - 'state': self.status, + 'status': self.status, 'msg': self.msg, 'mode': self.mode, 'linenumbers': self.linenumbers, @@ -492,7 +488,7 @@ class Schematic(InputTypeBase): context = {'id': self.id, 'value': self.value, 'initial_value': self.initial_value, - 'state': self.status, + 'status': self.status, 'width': self.width, 'height': self.height, 'parts': self.parts, @@ -542,7 +538,7 @@ class ImageInput(InputTypeBase): 'src': self.src, 'gx': self.gx, 'gy': self.gy, - 'state': self.status, # to change (VS: to what??) + 'status': self.status, # to change (VS: to what??) 'msg': self.msg, # to change } return context @@ -577,7 +573,7 @@ class Crystallography(InputTypeBase): def _get_render_context(self): context = {'id': self.id, 'value': self.value, - 'state': self.status, + 'status': self.status, 'size': self.size, 'msg': self.msg, 'hidden': self.hidden, @@ -614,7 +610,7 @@ class VseprInput(InputTypeBase): context = {'id': self.id, 'value': self.value, - 'state': self.status, + 'status': self.status, 'msg': self.msg, 'width': self.width, 'height': self.height, diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index ce1b6d9476..457d9e7817 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -1,12 +1,12 @@
- % if state == 'unsubmitted': + % if status == 'unsubmitted': - % elif state == 'correct': + % elif status == 'correct': - % elif state == 'incorrect': + % elif status == 'incorrect': - % elif state == 'incomplete': + % elif status == 'incomplete': % endif
diff --git a/common/lib/capa/capa/templates/codeinput.html b/common/lib/capa/capa/templates/codeinput.html index 6e44712d9f..5c2ff2aca5 100644 --- a/common/lib/capa/capa/templates/codeinput.html +++ b/common/lib/capa/capa/templates/codeinput.html @@ -6,13 +6,13 @@ >${value|h}
- % if state == 'unsubmitted': + % if status == 'unsubmitted': Unanswered - % elif state == 'correct': + % elif status == 'correct': Correct - % elif state == 'incorrect': + % elif status == 'incorrect': Incorrect - % elif state == 'queued': + % elif status == 'queued': Queued % endif @@ -21,7 +21,7 @@
% endif -

${state}

+

${status}

diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index ee9b249d3d..f46e2f753a 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -6,13 +6,13 @@
- % if state == 'unsubmitted': + % if status == 'unsubmitted':
- % elif state == 'correct': + % elif status == 'correct':
- % elif state == 'incorrect': + % elif status == 'incorrect':
- % elif state == 'incomplete': + % elif status == 'incomplete':
% endif % if hidden: @@ -29,13 +29,13 @@ />

- % 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

@@ -45,7 +45,7 @@ % if msg: ${msg|n} % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% endif diff --git a/common/lib/capa/capa/templates/filesubmission.html b/common/lib/capa/capa/templates/filesubmission.html index 630a3222dc..2572b25f8a 100644 --- a/common/lib/capa/capa/templates/filesubmission.html +++ b/common/lib/capa/capa/templates/filesubmission.html @@ -1,16 +1,16 @@
- % if state == 'unsubmitted': + % if status == 'unsubmitted': Unanswered - % elif state == 'correct': + % elif status == 'correct': Correct - % elif state == 'incorrect': + % elif status == 'incorrect': Incorrect - % elif state == 'queued': + % elif status == 'queued': Queued % endif -

${state}

+

${status}

diff --git a/common/lib/capa/capa/templates/imageinput.html b/common/lib/capa/capa/templates/imageinput.html index ceda98ee8f..97279c1b8c 100644 --- a/common/lib/capa/capa/templates/imageinput.html +++ b/common/lib/capa/capa/templates/imageinput.html @@ -4,13 +4,13 @@
- % if state == 'unsubmitted': + % if status == 'unsubmitted': - % elif state == 'correct': + % elif status == 'correct': - % elif state == 'incorrect': + % elif status == 'incorrect': - % elif state == 'incomplete': + % elif status == 'incomplete': % endif diff --git a/common/lib/capa/capa/templates/jstextline.html b/common/lib/capa/capa/templates/jstextline.html index 045f329ad4..7e5fd94a4b 100644 --- a/common/lib/capa/capa/templates/jstextline.html +++ b/common/lib/capa/capa/templates/jstextline.html @@ -18,13 +18,13 @@ % endif - % if state == 'unsubmitted': + % if status == 'unsubmitted': - % elif state == 'correct': + % elif status == 'correct': - % elif state == 'incorrect': + % elif status == 'incorrect': - % elif state == 'incomplete': + % elif status == 'incomplete': % endif % if msg: diff --git a/common/lib/capa/capa/templates/optioninput.html b/common/lib/capa/capa/templates/optioninput.html index 06c7ed1257..ac62c0c9cb 100644 --- a/common/lib/capa/capa/templates/optioninput.html +++ b/common/lib/capa/capa/templates/optioninput.html @@ -12,13 +12,13 @@ - % if state == 'unsubmitted': + % if status == 'unsubmitted': - % elif state == 'correct': + % elif status == 'correct': - % elif state == 'incorrect': + % elif status == 'incorrect': - % elif state == 'incomplete': + % elif status == 'incomplete': % endif diff --git a/common/lib/capa/capa/templates/schematicinput.html b/common/lib/capa/capa/templates/schematicinput.html index f79dc66d24..ff8cc64073 100644 --- a/common/lib/capa/capa/templates/schematicinput.html +++ b/common/lib/capa/capa/templates/schematicinput.html @@ -12,13 +12,13 @@ - % if state == 'unsubmitted': + % if status == 'unsubmitted': - % elif state == 'correct': + % elif status == 'correct': - % elif state == 'incorrect': + % elif status == 'incorrect': - % elif state == 'incomplete': + % elif status == 'incomplete': % endif diff --git a/common/lib/capa/capa/templates/textline.html b/common/lib/capa/capa/templates/textline.html index 3685742a3c..97c512fc00 100644 --- a/common/lib/capa/capa/templates/textline.html +++ b/common/lib/capa/capa/templates/textline.html @@ -7,13 +7,13 @@
% endif - % if state == 'unsubmitted': + % if status == 'unsubmitted':
- % elif state == 'correct': + % elif status == 'correct':
- % elif state == 'incorrect': + % elif status == 'incorrect':
- % elif state == 'incomplete': + % elif status == 'incomplete':
% endif % if hidden: @@ -33,13 +33,13 @@ />

- % 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

@@ -53,7 +53,7 @@ % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% endif diff --git a/common/lib/capa/capa/templates/vsepr_input.html b/common/lib/capa/capa/templates/vsepr_input.html index a6bd465554..eaa6ac2174 100644 --- a/common/lib/capa/capa/templates/vsepr_input.html +++ b/common/lib/capa/capa/templates/vsepr_input.html @@ -11,13 +11,13 @@
- % if state == 'unsubmitted': + % if status == 'unsubmitted':
- % elif state == 'correct': + % elif status == 'correct':
- % elif state == 'incorrect': + % elif status == 'incorrect':
- % elif state == 'incomplete': + % elif status == 'incomplete':
% endif @@ -26,13 +26,13 @@ />

- % 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

@@ -42,7 +42,7 @@ % if msg: ${msg|n} % endif -% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% endif diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 6c5b1cca15..65a22a876e 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -39,7 +39,7 @@ class OptionInputTest(unittest.TestCase): expected = {'value': 'Down', 'options': [('Up', 'Up'), ('Down', 'Down')], - 'state': 'answered', + 'status': 'answered', 'msg': '', 'inline': '', 'id': 'sky_input'} @@ -72,7 +72,7 @@ class ChoiceGroupTest(unittest.TestCase): expected = {'id': 'sky_input', 'value': 'foil3', - 'state': 'answered', + 'status': 'answered', 'input_type': expected_input_type, 'choices': [('foil1', 'This is foil One.'), ('foil2', 'This is foil Two.'), @@ -147,7 +147,7 @@ class TextLineTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': 'BumbleBee', - 'state': 'unanswered', + 'status': 'unanswered', 'size': size, 'msg': '', 'hidden': False, @@ -175,7 +175,7 @@ class TextLineTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': 'BumbleBee', - 'state': 'unanswered', + 'status': 'unanswered', 'size': size, 'msg': '', 'hidden': False, @@ -215,7 +215,7 @@ class FileSubmissionTest(unittest.TestCase): context = the_input._get_render_context() expected = {'id': 'prob_1_2', - 'state': 'queued', + 'status': 'queued', 'msg': 'Submitted to grader.', 'value': 'BumbleBee.py', 'queue_len': '3', @@ -260,7 +260,7 @@ class CodeInputTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': 'print "good evening"', - 'state': 'queued', + 'status': 'queued', 'msg': 'Submitted to grader.', 'mode': mode, 'linenumbers': linenumbers, @@ -311,7 +311,7 @@ class SchematicTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, 'initial_value': initial_value, - 'state': 'unsubmitted', + 'status': 'unsubmitted', 'width': width, 'height': height, 'parts': parts, @@ -349,13 +349,12 @@ class ImageInputTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, - 'state': 'unsubmitted', + 'status': 'unsubmitted', 'width': width, 'height': height, 'src': src, 'gx': egx, 'gy': egy, - 'state': 'unsubmitted', 'msg': ''} self.assertEqual(context, expected) @@ -404,7 +403,7 @@ class CrystallographyTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, - 'state': 'unsubmitted', + 'status': 'unsubmitted', 'size': size, 'msg': '', 'hidden': '', From b03789156c2e21cc4a98626c174c3bd1fd955aed Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 08:16:31 -0400 Subject: [PATCH 23/24] add test for vsepr --- common/lib/capa/capa/tests/test_inputtypes.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 65a22a876e..cab9bfe86b 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -414,6 +414,48 @@ class CrystallographyTest(unittest.TestCase): 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 = """""".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. From 6cc196394a9d845224fe0dae0da86cdac85f85e2 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Mon, 29 Oct 2012 12:06:42 -0400 Subject: [PATCH 24/24] fixing comments --- common/lib/capa/capa/inputtypes.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 38f2e5da8f..d47c5a3006 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -28,6 +28,12 @@ graded status as'status' # 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 @@ -214,7 +220,7 @@ class ChoiceGroup(InputTypeBase): 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 to this less hackish). + # value. (VS: would be nice to make this less hackish). if self.tag == 'choicegroup': self.suffix = '' self.element_type = "radio" @@ -333,8 +339,6 @@ class TextLine(InputTypeBase): # in div with name=self.hidden. self.hidden = self.xml.get('hidden', False) - # TODO (vshnayder): can we get rid of inline? Was it one of - # the styling hacks early this semester? self.inline = self.xml.get('inline', False) # TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x @@ -383,7 +387,7 @@ class FileSubmission(InputTypeBase): # 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.") - + def setup(self): escapedict = {'"': '"'} self.allowed_files = json.dumps(self.xml.get('allowed_files', '').split()) @@ -423,7 +427,8 @@ class CodeInput(InputTypeBase): template = "codeinput.html" tags = ['codeinput', - 'textbox', # Old name for this. Still supported, but deprecated. + 'textbox', # Another (older) name--at some point we may want to make it use a + # non-codemirror editor. ] @@ -526,7 +531,7 @@ class ImageInput(InputTypeBase): 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. - # (which supposedly has size 30). + # (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) @@ -541,8 +546,8 @@ class ImageInput(InputTypeBase): 'src': self.src, 'gx': self.gx, 'gy': self.gy, - 'status': self.status, # to change (VS: to what??) - 'msg': self.msg, # to change + 'status': self.status, + 'msg': self.msg, } return context