Commit 93cc9a8e by Victor Shnayder

formatting cleanup in responsetypes and inputtypes

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