Commit 93cc9a8e by Victor Shnayder

formatting cleanup in responsetypes and inputtypes

parent 40501258
......@@ -2,7 +2,7 @@
# File: courseware/capa/inputtypes.py
#
'''
"""
Module containing the problem elements which render into input objects
- textline
......@@ -16,15 +16,16 @@ Module containing the problem elements which render into input objects
- optioninput (for option list)
- filesubmission (upload a file)
These are matched by *.html files templates/*.html which are mako templates with the actual html.
These are matched by *.html files templates/*.html which are mako templates with the
actual html.
Each input type takes the xml tree as 'element', the previous answer as 'value', and the graded status as 'status'
Each input type takes the xml tree as 'element', the previous answer as 'value', and the
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: 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.
import logging
import re
......@@ -47,7 +48,8 @@ class SimpleInput():# XModule
Type for simple inputs -- plain HTML with a form element
'''
xml_tags = {} # # Maps tags to functions
# Maps tags to functions
xml_tags = {}
def __init__(self, system, xml, item_id=None, track_url=None, state=None, use='capa_input'):
'''
......@@ -69,19 +71,23 @@ class SimpleInput():# XModule
self.xml = xml
self.tag = xml.tag
self.system = system
if not state: state = {}
if not state:
state = {}
## ID should only come from one place.
## NOTE: ID should only come from one place.
## If it comes from multiple, we use state first, XML second, and parameter
## third. Since we don't make this guarantee, we can swap this around in
## the future if there's a more logical order.
if item_id: self.id = item_id
if xml.get('id'): self.id = xml.get('id')
if 'id' in state: self.id = state['id']
if item_id:
self.id = item_id
if xml.get('id'):
self.id = xml.get('id')
if 'id' in state:
self.id = state['id']
self.value = ''
if 'value' in state:
self.value = state['value']
self.value = state.get('value', '')
self.msg = ''
feedback = state.get('feedback')
......@@ -92,6 +98,7 @@ class SimpleInput():# XModule
# put hint above msg if to be displayed
if self.hintmode == 'always':
# TODO: is the '.' in <br/.> below a bug?
self.msg = self.hint + ('<br/.>' if self.msg else '') + self.msg
self.status = 'unanswered'
......@@ -107,7 +114,8 @@ class SimpleInput():# XModule
return ['capa_input', 'capa_transform']
def get_html(self):
return self.xml_tags[self.tag](self.xml, self.value, self.status, self.system.render_template, self.msg)
return self.xml_tags[self.tag](self.xml, self.value,
self.status, self.system.render_template, self.msg)
def register_render_function(fn, names=None, cls=SimpleInput):
......@@ -125,24 +133,26 @@ def register_render_function(fn, names=None, cls=SimpleInput):
@register_render_function
def optioninput(element, value, status, render_template, msg=''):
'''
"""
Select option input type.
Example:
<optioninput options="('Up','Down')" correct="Up"/><text>The location of the sky</text>
'''
"""
eid = element.get('id')
options = element.get('options')
if not options:
raise Exception("[courseware.capa.inputtypes.optioninput] Missing options specification in " + etree.tostring(element))
raise Exception(
"[courseware.capa.inputtypes.optioninput] Missing options specification in "
+ etree.tostring(element))
oset = shlex.shlex(options[1:-1])
oset.quotes = "'"
oset.whitespace = ","
oset = [x[1:-1] for x in list(oset)]
# osetdict = dict([('option_%s_%s' % (eid,x),oset[x]) for x in range(len(oset)) ]) # make dict with IDs
osetdict = [(oset[x], oset[x]) for x in range(len(oset))] # make ordered list with (key,value) same
# 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,
......@@ -166,26 +176,35 @@ def choicegroup(element, value, status, render_template, msg=''):
'''
Radio button 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.
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":
type = "radio"
element_type = "radio"
elif element.get('type') == "TrueFalse":
type = "checkbox"
element_type = "checkbox"
else:
type = "radio"
element_type = "radio"
choices = []
for choice in element:
if not choice.tag == 'choice':
raise Exception("[courseware.capa.inputtypes.choicegroup] Error only <choice> tags should be immediate children of a <choicegroup>, found %s instead" % choice.tag)
raise Exception("[courseware.capa.inputtypes.choicegroup] "
"Error: only <choice> tags should be immediate children "
"of a <choicegroup>, found %s instead" % choice.tag)
ctext = ""
ctext += ''.join([etree.tostring(x) for x in choice]) # TODO: what if choice[0] has math tags in it?
# 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:
ctext += choice.text # TODO: fix order?
# TODO: fix order?
ctext += choice.text
choices.append((choice.get("name"), ctext))
context = {'id': eid, 'value': value, 'state': status, 'input_type': type, 'choices': choices, 'name_array_suffix': ''}
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)
......@@ -196,8 +215,8 @@ def extract_choices(element):
Extracts choices for a few input types, such as radiogroup and
checkboxgroup.
TODO: allow order of choices to be randomized, following lon-capa spec. Use "location" attribute,
ie random, top, bottom.
TODO: allow order of choices to be randomized, following lon-capa spec. Use
"location" attribute, ie random, top, bottom.
'''
choices = []
......@@ -226,7 +245,12 @@ def radiogroup(element, value, status, render_template, msg=''):
choices = extract_choices(element)
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'radio', 'choices': choices, 'name_array_suffix': '[]'}
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)
......@@ -244,7 +268,12 @@ def checkboxgroup(element, value, status, render_template, msg=''):
choices = extract_choices(element)
context = {'id': eid, 'value': value, 'state': status, 'input_type': 'checkbox', 'choices': choices, 'name_array_suffix': '[]'}
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)
......@@ -260,49 +289,67 @@ def javascriptinput(element, value, status, render_template, msg='null'):
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'
escapedict = {'"': '&quot;'}
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,
'value': value, 'evaluation': msg,
context = {'id': eid,
'params': params,
'display_file': display_file,
'display_class': display_class,
'problem_state': problem_state,
'value': value,
'evaluation': msg,
}
html = render_template("javascriptinput.html", context)
return etree.XML(html)
@register_render_function
def textline(element, value, status, render_template, msg=""):
'''
Simple text line input, with optional size specification.
'''
if element.get('math') or element.get('dojs'): # 'dojs' flag is temporary, for backwards compatibility with 8.02x
return SimpleInput.xml_tags['textline_dynamath'](element, value, status, render_template, msg)
# TODO: 'dojs' flag is temporary, for backwards compatibility with 8.02x
if element.get('math') or element.get('dojs'):
return SimpleInput.xml_tags['textline_dynamath'](element, value, status,
render_template, msg)
eid = element.get('id')
if eid is None:
msg = 'textline has no id: it probably appears outside of a known response type'
msg += "\nSee problem XML source line %s" % getattr(element, 'sourceline', '<unavailable>')
raise Exception(msg)
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
hidden = element.get('hidden', '') # 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
hidden = element.get('hidden', '')
# Escape answers with quotes, so they don't crash the system!
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict) # otherwise, answers with quotes in them crashes the system!
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size, 'msg': msg, 'hidden': hidden,
value = saxutils.escape(value, escapedict)
context = {'id': eid,
'value': value,
'state': status,
'count': count,
'size': size,
'msg': msg,
'hidden': hidden,
'inline': element.get('inline',''),
}
html = render_template("textinput.html", context)
try:
xhtml = etree.XML(html)
except Exception as err:
if True: # TODO needs to be self.system.DEBUG - but can't access system
# 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
......@@ -313,7 +360,8 @@ def textline(element, value, status, render_template, msg=""):
@register_render_function
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).
Text line input with dynamic math display (equation rendered on client in real time
during input).
'''
# TODO: Make a wrapper for <formulainput>
# TODO: Make an AJAX loop to confirm equation is okay in real-time as user types
......@@ -325,7 +373,8 @@ def textline_dynamath(element, value, status, render_template, msg=''):
eid = element.get('id')
count = int(eid.split('_')[-2]) - 1 # HACK
size = element.get('size')
hidden = element.get('hidden', '') # 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
hidden = element.get('hidden', '')
# Preprocessor to insert between raw input and Mathjax
preprocessor = {'class_name': element.get('preprocessorClassName',''),
......@@ -337,10 +386,14 @@ def textline_dynamath(element, value, status, render_template, msg=''):
escapedict = {'"': '&quot;'}
value = saxutils.escape(value, escapedict)
context = {'id': eid, 'value': value, 'state': status, 'count': count, 'size': size,
'msg': msg, 'hidden': hidden,
'preprocessor': preprocessor,
}
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)
......@@ -360,15 +413,19 @@ def filesubmission(element, value, status, render_template, msg=''):
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
# Flag indicating that the problem has been queued, 'msg' is length of queue
if status == 'incomplete':
status = 'queued'
queue_len = msg
msg = 'Submitted to grader.'
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
'queue_len': queue_len, 'allowed_files': allowed_files,
'required_files': required_files
}
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)
......@@ -387,13 +444,17 @@ def textbox(element, value, status, render_template, msg=''):
size = element.get('size')
rows = element.get('rows') or '30'
cols = element.get('cols') or '80'
hidden = element.get('hidden', '') # 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
hidden = element.get('hidden', '')
if not value: value = element.text # 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 value:
value = element.text
# Check if problem has been queued
queue_len = 0
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
# Flag indicating that the problem has been queued, 'msg' is length of queue
if status == 'incomplete':
status = 'queued'
queue_len = msg
msg = 'Submitted to grader.'
......@@ -404,10 +465,18 @@ def textbox(element, value, status, render_template, msg=''):
tabsize = element.get('tabsize','4')
tabsize = int(tabsize)
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,
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,
}
html = render_template("textbox.html", context)
......@@ -475,7 +544,8 @@ def math(element, value, status, render_template, msg=''):
# mathstr = mathstr.replace('\\displaystyle','')
#else:
# isinline = True
# html = render_template("mathstring.html",{'mathstr':mathstr,'isinline':isinline,'tail':element.tail})
# html = render_template("mathstring.html", {'mathstr':mathstr,
# 'isinline':isinline,'tail':element.tail})
html = '<html><html>%s</html><html>%s</html></html>' % (mathstr, saxutils.escape(element.tail))
try:
......@@ -483,13 +553,14 @@ def math(element, value, status, render_template, msg=''):
except Exception as err:
if False: # TODO needs to be self.system.DEBUG - but can't access system
msg = '<html><div class="inline-error"><p>Error %s</p>' % str(err).replace('<', '&lt;')
msg += '<p>Failed to construct math expression from <pre>%s</pre></p>' % html.replace('<', '&lt;')
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
# xhtml.tail = element.tail # don't forget to include the tail!
# xhtml.tail = element.tail # don't forget to include the tail!
return xhtml
#-----------------------------------------------------------------------------
......@@ -520,11 +591,13 @@ def solution(element, value, status, render_template, msg=''):
@register_render_function
def imageinput(element, value, status, render_template, msg=''):
'''
Clickable image as an input field. Element should specify the image source, height, and width, eg
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="388" height="560" />
Clickable image as an input field. Element should specify the image source, height,
and width, e.g.
TODO: showanswer for imageimput does not work yet - need javascript to put rectangle over acceptable area of image.
<imageinput src="/static/Figures/Skier-conservation-of-energy.jpg" width="388" height="560" />
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')
......
......@@ -63,7 +63,7 @@ class StudentInputError(Exception):
class LoncapaResponse(object):
'''
"""
Base class for CAPA responsetypes. Each response type (ie a capa question,
which is part of a capa problem) is represented as a subclass,
which should provide the following methods:
......@@ -77,19 +77,29 @@ class LoncapaResponse(object):
In addition, these methods are optional:
- setup_response : find and note the answer input field IDs for the response; called by __init__
- check_hint_condition : check to see if the student's answers satisfy a particular condition for a hint to be displayed
- render_html : render this Response as HTML (must return XHTML compliant string)
- setup_response : find and note the answer input field IDs for the response; called
by __init__
- check_hint_condition : check to see if the student's answers satisfy a particular
condition for a hint to be displayed
- render_html : render this Response as HTML (must return XHTML-compliant string)
- __unicode__ : unicode representation of this Response
Each response type may also specify the following attributes:
- max_inputfields : (int) maximum number of answer input fields (checked in __init__ if not None)
- max_inputfields : (int) maximum number of answer input fields (checked in __init__
if not None)
- allowed_inputfields : list of allowed input fields (each a string) for this Response
- required_attributes : list of required attributes (each a string) on the main response XML stanza
- hint_tag : xhtml tag identifying hint associated with this response inside hintgroup
'''
- required_attributes : list of required attributes (each a string) on the main
response XML stanza
- hint_tag : xhtml tag identifying hint associated with this response inside
hintgroup
"""
__metaclass__ = abc.ABCMeta # abc = Abstract Base Class
response_tag = None
......@@ -121,26 +131,32 @@ class LoncapaResponse(object):
raise LoncapaProblemError(msg)
if self.max_inputfields and len(inputfields) > self.max_inputfields:
msg = "%s: cannot have more than %s input fields" % (unicode(self), self.max_inputfields)
msg = "%s: cannot have more than %s input fields" % (
unicode(self), self.max_inputfields)
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg)
for prop in self.required_attributes:
if not xml.get(prop):
msg = "Error in problem specification: %s missing required attribute %s" % (unicode(self), prop)
msg = "Error in problem specification: %s missing required attribute %s" % (
unicode(self), prop)
msg += "\nSee XML source line %s" % getattr(xml, 'sourceline', '<unavailable>')
raise LoncapaProblemError(msg)
self.answer_ids = [x.get('id') for x in self.inputfields] # ordered list of answer_id values for this response
# ordered list of answer_id values for this response
self.answer_ids = [x.get('id') for x in self.inputfields]
if self.max_inputfields == 1:
self.answer_id = self.answer_ids[0] # for convenience
# for convenience
self.answer_id = self.answer_ids[0]
self.maxpoints = dict()
for inputfield in self.inputfields:
maxpoints = inputfield.get('points','1') # By default, each answerfield is worth 1 point
# By default, each answerfield is worth 1 point
maxpoints = inputfield.get('points', '1')
self.maxpoints.update({inputfield.get('id'): int(maxpoints)})
self.default_answer_map = {} # dict for default answer map (provided in input elements)
# dict for default answer map (provided in input elements)
self.default_answer_map = {}
for entry in self.inputfields:
answer = entry.get('correct_answer')
if answer:
......@@ -163,10 +179,13 @@ class LoncapaResponse(object):
- renderer : procedure which produces HTML given an ElementTree
'''
tree = etree.Element('span') # render ourself as a <span> + our content
# render ourself as a <span> + our content
tree = etree.Element('span')
for item in self.xml:
item_xhtml = renderer(item) # call provided procedure to do the rendering
if item_xhtml is not None: tree.append(item_xhtml)
# call provided procedure to do the rendering
item_xhtml = renderer(item)
if item_xhtml is not None:
tree.append(item_xhtml)
tree.tail = self.xml.tail
return tree
......@@ -192,21 +211,21 @@ class LoncapaResponse(object):
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
'''
hintgroup = self.xml.find('hintgroup')
if hintgroup is None: return
if hintgroup is None:
return
# hint specified by function?
hintfn = hintgroup.get('hintfn')
if hintfn:
'''
Hint is determined by a function defined in the <script> context; evaluate that function to obtain
list of hint, hintmode for each answer_id.
Hint is determined by a function defined in the <script> context; evaluate
that function to obtain list of hint, hintmode for each answer_id.
The function should take arguments (answer_ids, student_answers, new_cmap, old_cmap)
and it should modify new_cmap as appropriate.
We may extend this in the future to add another argument which provides a callback procedure
to a social hint generation system.
We may extend this in the future to add another argument which provides a
callback procedure to a social hint generation system.
'''
if not hintfn in self.context:
msg = 'missing specified hint function %s in script context' % hintfn
......@@ -237,14 +256,20 @@ class LoncapaResponse(object):
# </hintgroup>
# </formularesponse>
if self.hint_tag is not None and hintgroup.find(self.hint_tag) is not None and hasattr(self, 'check_hint_condition'):
if (self.hint_tag is not None
and hintgroup.find(self.hint_tag) is not None
and hasattr(self, 'check_hint_condition')):
rephints = hintgroup.findall(self.hint_tag)
hints_to_show = self.check_hint_condition(rephints, student_answers)
hintmode = hintgroup.get('mode', 'always') # can be 'on_request' or 'always' (default)
# can be 'on_request' or 'always' (default)
hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show:
hint_text = hintpart.find('text').text
aid = self.answer_ids[-1] # make the hint appear after the last answer box in this response
# make the hint appear after the last answer box in this response
aid = self.answer_ids[-1]
new_cmap.set_hint_and_mode(aid, hint_text, hintmode)
log.debug('after hint: new_cmap = %s' % new_cmap)
......@@ -255,10 +280,10 @@ class LoncapaResponse(object):
(correctness, npoints, msg) for each answer_id.
Arguments:
- student_answers : dict of (answer_id,answer) where answer = student input (string)
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or recording history of responses
- old_cmap : previous CorrectMap (may be empty); useful for analyzing or
recording history of responses
'''
pass
......@@ -273,10 +298,13 @@ class LoncapaResponse(object):
'''
Return a list of hints to show.
- hxml_set : list of Element trees, each specifying a condition to be satisfied for a named hint condition
- hxml_set : list of Element trees, each specifying a condition to be
satisfied for a named hint condition
- student_answers : dict of student answers
Returns a list of names of hint conditions which were satisfied. Those are used to determine which hints are displayed.
Returns a list of names of hint conditions which were satisfied. Those are used
to determine which hints are displayed.
'''
pass
......@@ -290,10 +318,10 @@ class LoncapaResponse(object):
#-----------------------------------------------------------------------------
class JavascriptResponse(LoncapaResponse):
'''
"""
This response type is used when the student's answer is graded via
Javascript using Node.js.
'''
"""
response_tag = 'javascriptresponse'
max_inputfields = 1
......@@ -312,11 +340,11 @@ class JavascriptResponse(LoncapaResponse):
self.problem_state = self.generate_problem_state()
else:
self.problem_state = None
self.solution = None
self.prepare_inputfield()
def compile_display_javascript(self):
# TODO FIXME
......@@ -355,10 +383,10 @@ class JavascriptResponse(LoncapaResponse):
self.generator_xml = self.xml.xpath('//*[@id=$id]//generator',
id=self.xml.get('id'))[0]
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
self.grader_xml = self.xml.xpath('//*[@id=$id]//grader',
id=self.xml.get('id'))[0]
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
id=self.xml.get('id'))[0]
self.xml.remove(self.generator_xml)
......@@ -385,7 +413,7 @@ class JavascriptResponse(LoncapaResponse):
self.display_dependencies = []
self.display_class = self.display_xml.get("class")
def get_node_env(self):
js_dir = os.path.join(self.system.filestore.root_path, 'js')
......@@ -393,7 +421,7 @@ class JavascriptResponse(LoncapaResponse):
node_path = self.system.node_path + ":" + os.path.normpath(js_dir)
tmp_env["NODE_PATH"] = node_path
return tmp_env
def call_node(self, args):
subprocess_args = ["node"]
......@@ -406,7 +434,7 @@ class JavascriptResponse(LoncapaResponse):
generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
output = self.call_node([generator_file,
self.generator,
self.generator,
json.dumps(self.generator_dependencies),
json.dumps(str(self.context['the_lcp'].seed)),
json.dumps(self.params)]).strip()
......@@ -416,18 +444,18 @@ class JavascriptResponse(LoncapaResponse):
def extract_params(self):
params = {}
for param in self.xml.xpath('//*[@id=$id]//responseparam',
for param in self.xml.xpath('//*[@id=$id]//responseparam',
id=self.xml.get('id')):
raw_param = param.get("value")
params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context))
return params
def prepare_inputfield(self):
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
id=self.xml.get('id')):
escapedict = {'"': '&quot;'}
......@@ -454,36 +482,36 @@ class JavascriptResponse(LoncapaResponse):
else:
points = 0
return CorrectMap(self.answer_id, correctness, npoints=points, msg=evaluation)
def run_grader(self, submission):
if submission is None or submission == '':
submission = json.dumps(None)
grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
outputs = self.call_node([grader_file,
self.grader,
outputs = self.call_node([grader_file,
self.grader,
json.dumps(self.grader_dependencies),
submission,
json.dumps(self.problem_state),
submission,
json.dumps(self.problem_state),
json.dumps(self.params)]).split('\n')
all_correct = json.loads(outputs[0].strip())
evaluation = outputs[1].strip()
solution = outputs[2].strip()
return (all_correct, evaluation, solution)
def get_answers(self):
if self.solution is None:
(_, _, self.solution) = self.run_grader(None)
return {self.answer_id: self.solution}
#-----------------------------------------------------------------------------
class ChoiceResponse(LoncapaResponse):
'''
"""
This response type is used when the student chooses from a discrete set of
choices. Currently, to be marked correct, all "correct" choices must be
supplied by the student, and no extraneous choices may be included.
......@@ -528,7 +556,7 @@ class ChoiceResponse(LoncapaResponse):
choices must be given names. This behavior seems like a leaky abstraction,
and it'd be nice to change this at some point.
'''
"""
response_tag = 'choiceresponse'
max_inputfields = 1
......@@ -594,7 +622,8 @@ class MultipleChoiceResponse(LoncapaResponse):
allowed_inputfields = ['choicegroup']
def setup_response(self):
self.mc_setup_response() # call secondary setup for MultipleChoice questions, to set name attributes
# call secondary setup for MultipleChoice questions, to set name attributes
self.mc_setup_response()
# define correct choices (after calling secondary setup)
xml = self.xml
......@@ -609,7 +638,8 @@ class MultipleChoiceResponse(LoncapaResponse):
for response in self.xml.xpath("choicegroup"):
rtype = response.get('type')
if rtype not in ["MultipleChoice"]:
response.set("type", "MultipleChoice") # force choicegroup to be MultipleChoice if not valid
# force choicegroup to be MultipleChoice if not valid
response.set("type", "MultipleChoice")
for choice in list(response):
if choice.get("name") is None:
choice.set("name", "choice_" + str(i))
......@@ -621,8 +651,10 @@ class MultipleChoiceResponse(LoncapaResponse):
'''
grade student response.
'''
# log.debug('%s: student_answers=%s, correct_choices=%s' % (unicode(self),student_answers,self.correct_choices))
if self.answer_id in student_answers and student_answers[self.answer_id] in self.correct_choices:
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
# unicode(self), student_answers, self.correct_choices))
if (self.answer_id in student_answers
and student_answers[self.answer_id] in self.correct_choices):
return CorrectMap(self.answer_id, 'correct')
else:
return CorrectMap(self.answer_id, 'incorrect')
......@@ -662,10 +694,14 @@ class OptionResponse(LoncapaResponse):
'''
TODO: handle direction and randomize
'''
snippets = [{'snippet': '''<optionresponse direction="vertical" randomize="yes">
<optioninput options="('Up','Down')" correct="Up"><text>The location of the sky</text></optioninput>
<optioninput options="('Up','Down')" correct="Down"><text>The location of the earth</text></optioninput>
</optionresponse>'''}]
snippets = [{'snippet': """<optionresponse direction="vertical" randomize="yes">
<optioninput options="('Up','Down')" correct="Up">
<text>The location of the sky</text>
</optioninput>
<optioninput options="('Up','Down')" correct="Down">
<text>The location of the earth</text>
</optioninput>
</optionresponse>"""}]
response_tag = 'optionresponse'
hint_tag = 'optionhint'
......@@ -721,12 +757,13 @@ class NumericalResponse(LoncapaResponse):
'''Grade a numeric response '''
student_answer = student_answers[self.answer_id]
try:
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer), complex(self.correct_answer), self.tolerance)
correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer),
complex(self.correct_answer), self.tolerance)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except:
raise StudentInputError("Invalid input: could not interpret '%s' as a number" %\
raise StudentInputError("Invalid input: could not interpret '%s' as a number" %
cgi.escape(student_answer))
if correct:
......@@ -734,7 +771,7 @@ class NumericalResponse(LoncapaResponse):
else:
return CorrectMap(self.answer_id, 'incorrect')
# TODO: add check_hint_condition(self,hxml_set,student_answers)
# TODO: add check_hint_condition(self, hxml_set, student_answers)
def get_answers(self):
return {self.answer_id: self.correct_answer}
......@@ -784,7 +821,7 @@ class CustomResponse(LoncapaResponse):
Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
'''
snippets = [{'snippet': '''<customresponse>
snippets = [{'snippet': """<customresponse>
<text>
<br/>
Suppose that \(I(t)\) rises from \(0\) to \(I_S\) at a time \(t_0 \neq 0\)
......@@ -802,8 +839,8 @@ class CustomResponse(LoncapaResponse):
if not(r=="IS*u(t-t0)"):
correct[0] ='incorrect'
</answer>
</customresponse>'''},
{'snippet': '''<script type="loncapa/python"><![CDATA[
</customresponse>"""},
{'snippet': """<script type="loncapa/python"><![CDATA[
def sympy_check2():
messages[0] = '%s:%s' % (submission[0],fromjs[0].replace('<','&lt;'))
......@@ -816,7 +853,7 @@ def sympy_check2():
<customresponse cfn="sympy_check2" type="cs" expect="2.27E-39" dojs="math" size="30" answer="2.27E-39">
<textline size="40" dojs="math" />
<responseparam description="Numerical Tolerance" type="tolerance" default="0.00001" name="tol"/>
</customresponse>'''}]
</customresponse>"""}]
response_tag = 'customresponse'
allowed_inputfields = ['textline', 'textbox']
......@@ -830,7 +867,8 @@ def sympy_check2():
log.debug('answer_ids=%s' % self.answer_ids)
# the <answer>...</answer> stanza should be local to the current <customresponse>. So try looking there first.
# the <answer>...</answer> stanza should be local to the current <customresponse>.
# So try looking there first.
self.code = None
answer = None
try:
......@@ -838,8 +876,9 @@ def sympy_check2():
except IndexError:
# print "xml = ",etree.tostring(xml,pretty_print=True)
# if we have a "cfn" attribute then look for the function specified by cfn, in the problem context
# ie the comparison function is defined in the <script>...</script> stanza instead
# if we have a "cfn" attribute then look for the function specified by cfn, in
# the problem context ie the comparison function is defined in the
# <script>...</script> stanza instead
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
......@@ -847,13 +886,14 @@ def sympy_check2():
self.code = self.context[cfn]
else:
msg = "%s: can't find cfn %s in context" % (unicode(self), cfn)
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline', '<unavailable>')
msg += "\nSee XML source line %s" % getattr(self.xml, 'sourceline',
'<unavailable>')
raise LoncapaProblemError(msg)
if not self.code:
if answer is None:
# raise Exception,"[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid
log.error("[courseware.capa.responsetypes.customresponse] missing code checking script! id=%s" % self.myid)
log.error("[courseware.capa.responsetypes.customresponse] missing"
" code checking script! id=%s" % self.myid)
self.code = ''
else:
answer_src = answer.get('src')
......@@ -870,43 +910,70 @@ def sympy_check2():
log.debug('%s: student_answers=%s' % (unicode(self), student_answers))
idset = sorted(self.answer_ids) # ordered list of answer id's
# ordered list of answer id's
idset = sorted(self.answer_ids)
try:
submission = [student_answers[k] for k in idset] # ordered list of answers
# ordered list of answers
submission = [student_answers[k] for k in idset]
except Exception as err:
msg = '[courseware.capa.responsetypes.customresponse] error getting student answer from %s' % student_answers
msg = ('[courseware.capa.responsetypes.customresponse] error getting'
' student answer from %s' % student_answers)
msg += '\n idset = %s, error = %s' % (idset, err)
log.error(msg)
raise Exception(msg)
# global variable in context which holds the Presentation MathML from dynamic math input
dynamath = [student_answers.get(k + '_dynamath', None) for k in idset] # ordered list of dynamath responses
# ordered list of dynamath responses
dynamath = [student_answers.get(k + '_dynamath', None) for k in idset]
# if there is only one box, and it's empty, then don't evaluate
if len(idset) == 1 and not submission[0]:
# default to no error message on empty answer (to be consistent with other responsetypes)
# but allow author to still have the old behavior by setting empty_answer_err attribute
msg = '<span class="inline-error">No answer entered!</span>' if self.xml.get('empty_answer_err') else ''
# default to no error message on empty answer (to be consistent with other
# responsetypes) but allow author to still have the old behavior by setting
# empty_answer_err attribute
msg = ('<span class="inline-error">No answer entered!</span>'
if self.xml.get('empty_answer_err') else '')
return CorrectMap(idset[0], 'incorrect', msg=msg)
# NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's
# NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are
# not expecting 'unknown's
correct = ['unknown'] * len(idset)
messages = [''] * len(idset)
# put these in the context of the check function evaluator
# note that this doesn't help the "cfn" version - only the exec version
self.context.update({'xml': self.xml, # our subtree
'response_id': self.myid, # my ID
'expect': self.expect, # expected answer (if given as attribute)
'submission': submission, # ordered list of student answers from entry boxes in our subtree
'idset': idset, # ordered list of ID's of all entry boxes in our subtree
'dynamath': dynamath, # ordered list of all javascript inputs in our subtree
'answers': student_answers, # dict of student's responses, with keys being entry box IDs
'correct': correct, # the list to be filled in by the check function
'messages': messages, # the list of messages to be filled in by the check function
'options': self.xml.get('options'), # any options to be passed to the cfn
'testdat': 'hello world',
})
self.context.update({
# our subtree
'xml': self.xml,
# my ID
'response_id': self.myid,
# expected answer (if given as attribute)
'expect': self.expect,
# ordered list of student answers from entry boxes in our subtree
'submission': submission,
# ordered list of ID's of all entry boxes in our subtree
'idset': idset,
# ordered list of all javascript inputs in our subtree
'dynamath': dynamath,
# dict of student's responses, with keys being entry box IDs
'answers': student_answers,
# the list to be filled in by the check function
'correct': correct,
# the list of messages to be filled in by the check function
'messages': messages,
# any options to be passed to the cfn
'options': self.xml.get('options'),
'testdat': 'hello world',
})
# pass self.system.debug to cfn
self.context['debug'] = self.system.DEBUG
......@@ -921,8 +988,10 @@ def sympy_check2():
print "oops in customresponse (code) error %s" % err
print "context = ", self.context
print traceback.format_exc()
raise StudentInputError("Error: Problem could not be evaluated with your input") # Notify student
else: # self.code is not a string; assume its a function
# Notify student
raise StudentInputError("Error: Problem could not be evaluated with your input")
else:
# self.code is not a string; assume its a function
# this is an interface to the Tutor2 check functions
fn = self.code
......@@ -958,7 +1027,8 @@ def sympy_check2():
msg = '<html>' + msg + '</html>'
msg = msg.replace('&#60;', '&lt;')
#msg = msg.replace('&lt;','<')
msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True)
msg = etree.tostring(fromstring_bs(msg, convertEntities=None),
pretty_print=True)
#msg = etree.tostring(fromstring_bs(msg),pretty_print=True)
msg = msg.replace('&#13;', '')
#msg = re.sub('<html>(.*)</html>','\\1',msg,flags=re.M|re.DOTALL) # python 2.7
......@@ -1022,18 +1092,19 @@ class SymbolicResponse(CustomResponse):
class CodeResponse(LoncapaResponse):
'''
"""
Grade student code using an external queueing server, called 'xqueue'
Expects 'xqueue' dict in ModuleSystem with the following keys that are needed by CodeResponse:
system.xqueue = { 'interface': XqueueInterface object,
'callback_url': Per-StudentModule callback URL where results are posted (string),
'callback_url': Per-StudentModule callback URL
where results are posted (string),
'default_queuename': Default queuename to submit request (string)
}
External requests are only submitted for student submission grading
External requests are only submitted for student submission grading
(i.e. and not for getting reference answers)
'''
"""
response_tag = 'coderesponse'
allowed_inputfields = ['textbox', 'filesubmission']
......@@ -1046,7 +1117,8 @@ class CodeResponse(LoncapaResponse):
TODO: Determines whether in synchronous or asynchronous (queued) mode
'''
xml = self.xml
self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL
# TODO: XML can override external resource (grader/queue) URL
self.url = xml.get('url', None)
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
# VS[compat]:
......@@ -1107,7 +1179,8 @@ class CodeResponse(LoncapaResponse):
# Extract 'answer' and 'initial_display' from XML. Note that the code to be exec'ed here is:
# (1) Internal edX code, i.e. NOT student submissions, and
# (2) The code should only define the strings 'initial_display', 'answer', 'preamble', 'test_program'
# (2) The code should only define the strings 'initial_display', 'answer',
# 'preamble', 'test_program'
# following the ExternalResponse XML format
penv = {}
penv['__builtins__'] = globals()['__builtins__']
......@@ -1120,10 +1193,12 @@ class CodeResponse(LoncapaResponse):
self.answer = penv['answer']
self.initial_display = penv['initial_display']
except Exception as err:
log.error("Error in CodeResponse %s: Problem reference code does not define 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
log.error("Error in CodeResponse %s: Problem reference code does not define"
" 'answer' and/or 'initial_display' in <answer>...</answer>" % err)
raise Exception(err)
# Finally, make the ExternalResponse input XML format conform to the generic exteral grader interface
# Finally, make the ExternalResponse input XML format conform to the generic
# exteral grader interface
# The XML tagging of grader_payload is pyxserver-specific
grader_payload = '<pyxserver>'
grader_payload += '<tests>' + tests + '</tests>\n'
......@@ -1133,14 +1208,16 @@ class CodeResponse(LoncapaResponse):
def get_score(self, student_answers):
try:
submission = student_answers[self.answer_id] # Note that submission can be a file
# Note that submission can be a file
submission = student_answers[self.answer_id]
except Exception as err:
log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' %
log.error('Error in CodeResponse %s: cannot get student answer for %s;'
' student_answers=%s' %
(err, self.answer_id, convert_files_to_filenames(student_answers)))
raise Exception(err)
# Prepare xqueue request
#------------------------------------------------------------
#------------------------------------------------------------
qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
......@@ -1149,19 +1226,20 @@ class CodeResponse(LoncapaResponse):
# Generate header
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
anonymous_student_id +
anonymous_student_id +
self.answer_id)
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
lms_key=queuekey,
queue_name=self.queue_name)
# Generate body
if is_list_of_files(submission):
self.context.update({'submission': ''}) # TODO: Get S3 pointer from the Queue
# TODO: Get S3 pointer from the Queue
self.context.update({'submission': ''})
else:
self.context.update({'submission': submission})
contents = self.payload.copy()
contents = self.payload.copy()
# Metadata related to the student submission revealed to the external grader
student_info = {'anonymous_student_id': anonymous_student_id,
......@@ -1171,7 +1249,8 @@ class CodeResponse(LoncapaResponse):
# Submit request. When successful, 'msg' is the prior length of the queue
if is_list_of_files(submission):
contents.update({'student_response': ''}) # TODO: Is there any information we want to send here?
# TODO: Is there any information we want to send here?
contents.update({'student_response': ''})
(error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents),
files_to_upload=submission)
......@@ -1182,44 +1261,51 @@ class CodeResponse(LoncapaResponse):
# State associated with the queueing request
queuestate = {'key': queuekey,
'time': qtime,
}
'time': qtime,}
cmap = CorrectMap()
cmap = CorrectMap()
if error:
cmap.set(self.answer_id, queuestate=None,
msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg)
msg='Unable to deliver your submission to grader. (Reason: %s.)'
' Please try again later.' % msg)
else:
# Queueing mechanism flags:
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
# and .filesubmission to inform the browser to poll the LMS
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
# the problem has been queued
# 2) Frontend: correctness='incomplete' eventually trickles down
# through inputtypes.textbox and .filesubmission to inform the
# browser to poll the LMS
cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg)
return cmap
def update_score(self, score_msg, oldcmap, queuekey):
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
if not valid_score_msg:
oldcmap.set(self.answer_id, msg='Invalid grader reply. Please contact the course staff.')
oldcmap.set(self.answer_id,
msg='Invalid grader reply. Please contact the course staff.')
return oldcmap
correctness = 'correct' if correct else 'incorrect'
self.context['correct'] = correctness # TODO: Find out how this is used elsewhere, if any
# TODO: Find out how this is used elsewhere, if any
self.context['correct'] = correctness
# Replace 'oldcmap' with new grading results if queuekey matches.
# If queuekey does not match, we keep waiting for the score_msg whose key actually matches
# Replace 'oldcmap' with new grading results if queuekey matches. If queuekey
# does not match, we keep waiting for the score_msg whose key actually matches
if oldcmap.is_right_queuekey(self.answer_id, queuekey):
# Sanity check on returned points
# Sanity check on returned points
if points < 0:
points = 0
elif points > self.maxpoints[self.answer_id]:
points = self.maxpoints[self.answer_id]
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace('&nbsp;', '&#160;'), queuestate=None) # Queuestate is consumed
# Queuestate is consumed
oldcmap.set(self.answer_id, npoints=points, correctness=correctness,
msg=msg.replace('&nbsp;', '&#160;'), queuestate=None)
else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' %
(queuekey, self.answer_id))
return oldcmap
......@@ -1231,7 +1317,7 @@ class CodeResponse(LoncapaResponse):
return {self.answer_id: self.initial_display}
def _parse_score_msg(self, score_msg):
'''
"""
Grader reply is a JSON-dump of the following dict
{ 'correct': True/False,
'score': Numeric value (floating point is okay) to assign to answer
......@@ -1242,22 +1328,25 @@ class CodeResponse(LoncapaResponse):
correct: Correctness of submission (Boolean)
score: Points to be assigned (numeric, can be float)
msg: Message from grader to display to student (string)
'''
"""
fail = (False, False, 0, '')
try:
score_result = json.loads(score_msg)
except (TypeError, ValueError):
log.error("External grader message should be a JSON-serialized dict. Received score_msg = %s" % score_msg)
log.error("External grader message should be a JSON-serialized dict."
" Received score_msg = %s" % score_msg)
return fail
if not isinstance(score_result, dict):
log.error("External grader message should be a JSON-serialized dict. Received score_result = %s" % score_result)
log.error("External grader message should be a JSON-serialized dict."
" Received score_result = %s" % score_result)
return fail
for tag in ['correct', 'score', 'msg']:
if tag not in score_result:
log.error("External grader message is missing one or more required tags: 'correct', 'score', 'msg'")
log.error("External grader message is missing one or more required"
" tags: 'correct', 'score', 'msg'")
return fail
# Next, we need to check that the contents of the external grader message
# Next, we need to check that the contents of the external grader message
# is safe for the LMS.
# 1) Make sure that the message is valid XML (proper opening/closing tags)
# 2) TODO: Is the message actually HTML?
......@@ -1265,11 +1354,12 @@ class CodeResponse(LoncapaResponse):
try:
etree.fromstring(msg)
except etree.XMLSyntaxError as err:
log.error("Unable to parse external grader message as valid XML: score_msg['msg']=%s" % msg)
log.error("Unable to parse external grader message as valid"
" XML: score_msg['msg']=%s" % msg)
return fail
return (True, score_result['correct'], score_result['score'], msg)
#-----------------------------------------------------------------------------
......@@ -1325,9 +1415,9 @@ main()
def setup_response(self):
xml = self.xml
self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
# FIXME - hardcoded URL
self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa"
# answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors
answer = xml.find('answer')
if answer is not None:
answer_src = answer.get('src')
......@@ -1335,7 +1425,8 @@ main()
self.code = self.system.filesystem.open('src/' + answer_src).read()
else:
self.code = answer.text
else: # no <answer> stanza; get code from <script>
else:
# no <answer> stanza; get code from <script>
self.code = self.context['script_code']
if not self.code:
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
......@@ -1362,19 +1453,22 @@ main()
payload.update(extra_payload)
try:
r = requests.post(self.url, data=payload) # call external server
# call external server. TODO: synchronous call, can block for a long time
r = requests.post(self.url, data=payload)
except Exception as err:
msg = 'Error %s - cannot connect to external server url=%s' % (err, self.url)
log.error(msg)
raise Exception(msg)
if self.system.DEBUG: log.info('response = %s' % r.text)
if self.system.DEBUG:
log.info('response = %s' % r.text)
if (not r.text) or (not r.text.strip()):
raise Exception('Error: no response from external server url=%s' % self.url)
try:
rxml = etree.fromstring(r.text) # response is XML; prase it
# response is XML; parse it
rxml = etree.fromstring(r.text)
except Exception as err:
msg = 'Error %s - cannot parse response from external server r.text=%s' % (err, r.text)
log.error(msg)
......@@ -1388,7 +1482,8 @@ main()
try:
submission = [student_answers[k] for k in idset]
except Exception as err:
log.error('Error %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_ids, student_answers))
log.error('Error %s: cannot get student answer for %s; student_answers=%s' %
(err, self.answer_ids, student_answers))
raise Exception(err)
self.context.update({'submission': submission})
......@@ -1401,7 +1496,9 @@ main()
log.error('Error %s' % err)
if self.system.DEBUG:
cmap.set_dict(dict(zip(sorted(self.answer_ids), ['incorrect'] * len(idset))))
cmap.set_property(self.answer_ids[0], 'msg', '<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;'))
cmap.set_property(
self.answer_ids[0], 'msg',
'<span class="inline-error">%s</span>' % str(err).replace('<', '&lt;'))
return cmap
ad = rxml.find('awarddetail').text
......@@ -1435,7 +1532,8 @@ main()
exans[0] = msg
if not (len(exans) == len(self.answer_ids)):
log.error('Expected %d answers from external server, only got %d!' % (len(self.answer_ids), len(exans)))
log.error('Expected %d answers from external server, only got %d!' %
(len(self.answer_ids), len(exans)))
raise Exception('Short response from external server')
return dict(zip(self.answer_ids, exans))
......@@ -1487,11 +1585,14 @@ class FormulaResponse(LoncapaResponse):
typeslist = []
else:
typeslist = ts.split(',')
if 'ci' in typeslist: # Case insensitive
if 'ci' in typeslist:
# Case insensitive
self.case_sensitive = False
elif 'cs' in typeslist: # Case sensitive
elif 'cs' in typeslist:
# Case sensitive
self.case_sensitive = True
else: # Default
else:
# Default
self.case_sensitive = False
def get_score(self, student_answers):
......@@ -1509,12 +1610,14 @@ class FormulaResponse(LoncapaResponse):
for i in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict()
for var in ranges: # ranges give numerical ranges for testing
# ranges give numerical ranges for testing
for var in ranges:
value = random.uniform(*ranges[var])
instructor_variables[str(var)] = value
student_variables[str(var)] = value
#log.debug('formula: instructor_vars=%s, expected=%s' % (instructor_variables,expected))
instructor_result = evaluator(instructor_variables, dict(), expected, cs=self.case_sensitive)
instructor_result = evaluator(instructor_variables, dict(),
expected, cs=self.case_sensitive)
try:
#log.debug('formula: student_vars=%s, given=%s' % (student_variables,given))
student_result = evaluator(student_variables,
......@@ -1540,9 +1643,9 @@ class FormulaResponse(LoncapaResponse):
keys and all non-numeric values stripped out. All values also
converted to float. Used so we can safely use Python contexts.
'''
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and \
k.isalnum() and \
isinstance(d[k], numbers.Number)])
d = dict([(k, numpy.complex(d[k])) for k in d if type(k) == str and
k.isalnum() and
isinstance(d[k], numbers.Number)])
return d
def check_hint_condition(self, hxml_set, student_answers):
......@@ -1577,7 +1680,8 @@ class SchematicResponse(LoncapaResponse):
answer = xml.xpath('//*[@id=$id]//answer', id=xml.get('id'))[0]
answer_src = answer.get('src')
if answer_src is not None:
self.code = self.system.filestore.open('src/' + answer_src).read() # Untested; never used
# Untested; never used
self.code = self.system.filestore.open('src/' + answer_src).read()
else:
self.code = answer.text
......@@ -1633,17 +1737,19 @@ class ImageResponse(LoncapaResponse):
# parse expected answer
# TODO: Compile regexp on file load
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]', expectedset[aid].strip().replace(' ', ''))
m = re.match('[\(\[]([0-9]+),([0-9]+)[\)\]]-[\(\[]([0-9]+),([0-9]+)[\)\]]',
expectedset[aid].strip().replace(' ', ''))
if not m:
msg = 'Error in problem specification! cannot parse rectangle in %s' % (etree.tostring(self.ielements[aid],
pretty_print=True))
msg = 'Error in problem specification! cannot parse rectangle in %s' % (
etree.tostring(self.ielements[aid], pretty_print=True))
raise Exception('[capamodule.capa.responsetypes.imageinput] ' + msg)
(llx, lly, urx, ury) = [int(x) for x in m.groups()]
# parse given answer
m = re.match('\[([0-9]+),([0-9]+)]', given.strip().replace(' ', ''))
if not m:
raise Exception('[capamodule.capa.responsetypes.imageinput] error grading %s (input=%s)' % (aid, given))
raise Exception('[capamodule.capa.responsetypes.imageinput] '
'error grading %s (input=%s)' % (aid, given))
(gx, gy) = [int(x) for x in m.groups()]
# answer is correct if (x,y) is within the specified rectangle
......@@ -1660,4 +1766,17 @@ class ImageResponse(LoncapaResponse):
# TEMPORARY: List of all response subclasses
# FIXME: To be replaced by auto-registration
__all__ = [CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse, ChoiceResponse, MultipleChoiceResponse, TrueFalseResponse, JavascriptResponse]
__all__ = [CodeResponse,
NumericalResponse,
FormulaResponse,
CustomResponse,
SchematicResponse,
ExternalResponse,
ImageResponse,
OptionResponse,
SymbolicResponse,
StringResponse,
ChoiceResponse,
MultipleChoiceResponse,
TrueFalseResponse,
JavascriptResponse]
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