Commit d6cb4328 by Victor Shnayder

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
parent 1a843c2d
...@@ -32,13 +32,14 @@ graded status as'status' ...@@ -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 # TODO: Quoting and unquoting is handled in a pretty ad-hoc way. Also something that could be done
# properly once in InputTypeBase. # properly once in InputTypeBase.
import json
import logging import logging
from lxml import etree
import re import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import json import sys
from lxml import etree
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
from registry import TagRegistry from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -99,6 +100,26 @@ class InputTypeBase(object): ...@@ -99,6 +100,26 @@ class InputTypeBase(object):
self.status = state.get('status', 'unanswered') 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): def _get_render_context(self):
""" """
Abstract method. Subclasses should implement to return the dictionary Abstract method. Subclasses should implement to return the dictionary
...@@ -135,15 +156,11 @@ class OptionInput(InputTypeBase): ...@@ -135,15 +156,11 @@ class OptionInput(InputTypeBase):
template = "optioninput.html" template = "optioninput.html"
tags = ['optioninput'] tags = ['optioninput']
def __init__(self, system, xml, state): def setup(self):
super(OptionInput, self).__init__(system, xml, state)
# Extract the options... # Extract the options...
options = self.xml.get('options') options = self.xml.get('options')
if not options: if not options:
raise Exception( raise ValueError("optioninput: Missing 'options' specification.")
"[courseware.capa.inputtypes.optioninput] Missing options specification in "
+ etree.tostring(self.xml))
# parse the set of possible options # parse the set of possible options
oset = shlex.shlex(options[1:-1]) oset = shlex.shlex(options[1:-1])
...@@ -199,9 +216,7 @@ class ChoiceGroup(InputTypeBase): ...@@ -199,9 +216,7 @@ class ChoiceGroup(InputTypeBase):
template = "choicegroup.html" template = "choicegroup.html"
tags = ['choicegroup', 'radiogroup', 'checkboxgroup'] tags = ['choicegroup', 'radiogroup', 'checkboxgroup']
def __init__(self, system, xml, state): def setup(self):
super(ChoiceGroup, self).__init__(system, xml, state)
# suffix is '' or [] to change the way the input is handled in --as a scalar or vector # 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 to this less hackish).
if self.tag == 'choicegroup': if self.tag == 'choicegroup':
...@@ -242,9 +257,9 @@ def extract_choices(element): ...@@ -242,9 +257,9 @@ def extract_choices(element):
for choice in element: for choice in element:
if choice.tag != 'choice': if choice.tag != 'choice':
raise Exception("[courseware.capa.inputtypes.extract_choices] \ raise Exception(
Expected a <choice> tag; got %s instead" "[capa.inputtypes.extract_choices] Expected a <choice> tag; got %s instead"
% choice.tag) % choice.tag)
choice_text = ''.join([etree.tostring(x) for x in choice]) choice_text = ''.join([etree.tostring(x) for x in choice])
if choice.text is not None: if choice.text is not None:
# TODO: fix order? # TODO: fix order?
...@@ -270,8 +285,7 @@ class JavascriptInput(InputTypeBase): ...@@ -270,8 +285,7 @@ class JavascriptInput(InputTypeBase):
template = "javascriptinput.html" template = "javascriptinput.html"
tags = ['javascriptinput'] tags = ['javascriptinput']
def __init__(self, system, xml, state): def setup(self):
super(JavascriptInput, self).__init__(system, xml, state)
# Need to provide a value that JSON can parse if there is no # Need to provide a value that JSON can parse if there is no
# student-supplied value yet. # student-supplied value yet.
if self.value == "": if self.value == "":
...@@ -311,8 +325,7 @@ class TextLine(InputTypeBase): ...@@ -311,8 +325,7 @@ class TextLine(InputTypeBase):
template = "textinput.html" template = "textinput.html"
tags = ['textline'] tags = ['textline']
def __init__(self, system, xml, state): def setup(self):
super(TextLine, self).__init__(system, xml, state)
self.size = self.xml.get('size') self.size = self.xml.get('size')
# if specified, then textline is hidden and input id is stored # if specified, then textline is hidden and input id is stored
...@@ -366,12 +379,11 @@ class FileSubmission(InputTypeBase): ...@@ -366,12 +379,11 @@ class FileSubmission(InputTypeBase):
template = "filesubmission.html" template = "filesubmission.html"
tags = ['filesubmission'] tags = ['filesubmission']
def __init__(self, system, xml, state): def setup(self):
super(FileSubmission, self).__init__(system, xml, state)
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
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.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) self.required_files = saxutils.escape(self.required_files, escapedict)
# Check if problem has been queued # Check if problem has been queued
...@@ -410,17 +422,16 @@ class CodeInput(InputTypeBase): ...@@ -410,17 +422,16 @@ class CodeInput(InputTypeBase):
'textbox', # Old name for this. Still supported, but deprecated. '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' def setup(self):
self.cols = xml.get('cols') or '80' 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 # 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 no student input yet, then use the default input given by the problem
if not self.value: if not self.value:
self.value = xml.text self.value = self.xml.text
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0 self.queue_len = 0
...@@ -431,9 +442,9 @@ class CodeInput(InputTypeBase): ...@@ -431,9 +442,9 @@ class CodeInput(InputTypeBase):
self.msg = 'Submitted to grader.' self.msg = 'Submitted to grader.'
# For CodeMirror # For CodeMirror
self.mode = xml.get('mode', 'python') self.mode = self.xml.get('mode', 'python')
self.linenumbers = xml.get('linenumbers', 'true') self.linenumbers = self.xml.get('linenumbers', 'true')
self.tabsize = int(xml.get('tabsize', '4')) self.tabsize = int(self.xml.get('tabsize', '4'))
def _get_render_context(self): def _get_render_context(self):
...@@ -462,14 +473,13 @@ class Schematic(InputTypeBase): ...@@ -462,14 +473,13 @@ class Schematic(InputTypeBase):
template = "schematicinput.html" template = "schematicinput.html"
tags = ['schematic'] tags = ['schematic']
def __init__(self, system, xml, state): def setup(self):
super(Schematic, self).__init__(system, xml, state) self.height = self.xml.get('height')
self.height = xml.get('height') self.width = self.xml.get('width')
self.width = xml.get('width') self.parts = self.xml.get('parts')
self.parts = xml.get('parts') self.analyses = self.xml.get('analyses')
self.analyses = xml.get('analyses') self.initial_value = self.xml.get('initial_value')
self.initial_value = xml.get('initial_value') self.submit_analyses = self.xml.get('submit_analyses')
self.submit_analyses = xml.get('submit_analyses')
def _get_render_context(self): def _get_render_context(self):
...@@ -482,7 +492,7 @@ class Schematic(InputTypeBase): ...@@ -482,7 +492,7 @@ class Schematic(InputTypeBase):
'height': self.height, 'height': self.height,
'parts': self.parts, 'parts': self.parts,
'analyses': self.analyses, 'analyses': self.analyses,
'submit_analyses': self.submit_analyses, } 'submit_analyses': self.submit_analyses,}
return context return context
registry.register(Schematic) registry.register(Schematic)
...@@ -503,11 +513,10 @@ class ImageInput(InputTypeBase): ...@@ -503,11 +513,10 @@ class ImageInput(InputTypeBase):
template = "imageinput.html" template = "imageinput.html"
tags = ['imageinput'] tags = ['imageinput']
def __init__(self, system, xml, state): def setup(self):
super(ImageInput, self).__init__(system, xml, state) self.src = self.xml.get('src')
self.src = xml.get('src') self.height = self.xml.get('height')
self.height = xml.get('height') self.width = self.xml.get('width')
self.width = xml.get('width')
# if value is of the form [x,y] then parse it and send along coordinates of previous answer # 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(' ', '')) m = re.match('\[([0-9]+),([0-9]+)]', self.value.strip().replace(' ', ''))
...@@ -547,15 +556,14 @@ class Crystallography(InputTypeBase): ...@@ -547,15 +556,14 @@ class Crystallography(InputTypeBase):
template = "crystallography.html" template = "crystallography.html"
tags = ['crystallography'] tags = ['crystallography']
def __init__(self, system, xml, state):
super(Crystallography, self).__init__(system, xml, state)
self.height = xml.get('height') def setup(self):
self.width = xml.get('width') self.height = self.xml.get('height')
self.size = xml.get('size') 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 # 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! # Escape answers with quotes, so they don't crash the system!
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
...@@ -586,18 +594,16 @@ class VseprInput(InputTypeBase): ...@@ -586,18 +594,16 @@ class VseprInput(InputTypeBase):
template = 'vsepr_input.html' template = 'vsepr_input.html'
tags = ['vsepr_input'] tags = ['vsepr_input']
def __init__(self, system, xml, state): def setup(self):
super(VseprInput, self).__init__(system, xml, state) self.height = self.xml.get('height')
self.width = self.xml.get('width')
self.height = xml.get('height')
self.width = xml.get('width')
# Escape answers with quotes, so they don't crash the system! # Escape answers with quotes, so they don't crash the system!
escapedict = {'"': '&quot;'} escapedict = {'"': '&quot;'}
self.value = saxutils.escape(self.value, escapedict) self.value = saxutils.escape(self.value, escapedict)
self.molecules = xml.get('molecules') self.molecules = self.xml.get('molecules')
self.geometries = xml.get('geometries') self.geometries = self.xml.get('geometries')
def _get_render_context(self): def _get_render_context(self):
...@@ -631,13 +637,15 @@ class ChemicalEquationInput(InputTypeBase): ...@@ -631,13 +637,15 @@ class ChemicalEquationInput(InputTypeBase):
template = "chemicalequationinput.html" template = "chemicalequationinput.html"
tags = ['chemicalequationinput'] tags = ['chemicalequationinput']
def setup(self):
self.size = self.xml.get('size', '20')
def _get_render_context(self): def _get_render_context(self):
size = self.xml.get('size', '20')
context = { context = {
'id': self.id, 'id': self.id,
'value': self.value, 'value': self.value,
'status': self.status, 'status': self.status,
'size': size, 'size': self.size,
'previewer': '/static/js/capa/chemical_equation_preview.js', 'previewer': '/static/js/capa/chemical_equation_preview.js',
} }
return context return context
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment