externalresponse fixes; clean up semantics of interface, add command
for getting expected answer from external server, add error handling, extend to use json for multiple input fields, added rows & cols to textbox inputtype for code input.
This commit is contained in:
@@ -253,16 +253,19 @@ def textbox(element, value, status, msg=''):
|
|||||||
The textbox is used for code input. The message is the return HTML string from
|
The textbox is used for code input. The message is the return HTML string from
|
||||||
evaluating the code, eg error messages, and output from the code tests.
|
evaluating the code, eg error messages, and output from the code tests.
|
||||||
|
|
||||||
TODO: make this use rows and cols attribs, not size
|
|
||||||
'''
|
'''
|
||||||
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')
|
||||||
|
rows = element.get('rows') or '30'
|
||||||
|
cols = element.get('cols') or '80'
|
||||||
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
|
mode = element.get('mode') or 'python' # mode for CodeMirror, eg "python" or "xml"
|
||||||
linenumbers = element.get('linenumbers') # for CodeMirror
|
linenumbers = element.get('linenumbers') # for CodeMirror
|
||||||
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
if not value: value = element.text # if no student input yet, then use the default input given by the problem
|
||||||
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
|
context = {'id':eid, 'value':value, 'state':status, 'count':count, 'size': size, 'msg':msg,
|
||||||
'mode':mode, 'linenumbers':linenumbers }
|
'mode':mode, 'linenumbers':linenumbers,
|
||||||
|
'rows':rows, 'cols':cols,
|
||||||
|
}
|
||||||
html=render_to_string("textbox.html", context)
|
html=render_to_string("textbox.html", context)
|
||||||
return etree.XML(html)
|
return etree.XML(html)
|
||||||
|
|
||||||
|
|||||||
@@ -471,13 +471,55 @@ class SymbolicResponse(CustomResponse):
|
|||||||
#-----------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------
|
||||||
|
|
||||||
class ExternalResponse(GenericResponse):
|
class ExternalResponse(GenericResponse):
|
||||||
"""
|
'''
|
||||||
Grade the student's input using an external server.
|
Grade the students input using an external server.
|
||||||
|
|
||||||
Typically used by coding problems.
|
Typically used by coding problems.
|
||||||
"""
|
|
||||||
|
Example:
|
||||||
|
<externalresponse tests="repeat:10,generate">
|
||||||
|
<textbox rows="10" cols="70" mode="python"/>
|
||||||
|
<answer><![CDATA[
|
||||||
|
initial_display = """
|
||||||
|
def inc(x):
|
||||||
|
"""
|
||||||
|
|
||||||
|
answer = """
|
||||||
|
def inc(n):
|
||||||
|
return n+1
|
||||||
|
"""
|
||||||
|
preamble = """
|
||||||
|
import sympy
|
||||||
|
"""
|
||||||
|
test_program = """
|
||||||
|
import random
|
||||||
|
|
||||||
|
def testInc(n = None):
|
||||||
|
if n is None:
|
||||||
|
n = random.randint(2, 20)
|
||||||
|
print 'Test is: inc(%d)'%n
|
||||||
|
return str(inc(n))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
f = os.fdopen(3,'w')
|
||||||
|
test = int(sys.argv[1])
|
||||||
|
rndlist = map(int,os.getenv('rndlist').split(','))
|
||||||
|
random.seed(rndlist[0])
|
||||||
|
if test == 1: f.write(testInc(0))
|
||||||
|
elif test == 2: f.write(testInc(1))
|
||||||
|
else: f.write(testInc())
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
main()
|
||||||
|
"""
|
||||||
|
]]>
|
||||||
|
</answer>
|
||||||
|
</externalresponse>
|
||||||
|
|
||||||
|
'''
|
||||||
def __init__(self, xml, context, system=None):
|
def __init__(self, xml, context, system=None):
|
||||||
self.xml = xml
|
self.xml = xml
|
||||||
|
self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL
|
||||||
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
|
self.answer_ids = xml.xpath('//*[@id=$id]//textbox/@id|//*[@id=$id]//textline/@id',
|
||||||
id=xml.get('id'))
|
id=xml.get('id'))
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -490,24 +532,29 @@ class ExternalResponse(GenericResponse):
|
|||||||
else:
|
else:
|
||||||
self.code = answer.text
|
self.code = answer.text
|
||||||
|
|
||||||
self.tests = xml.get('answer')
|
self.tests = xml.get('tests')
|
||||||
|
|
||||||
def get_score(self, student_answers):
|
def get_score(self, student_answers):
|
||||||
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
try:
|
||||||
|
submission = [student_answers[k] for k in sorted(self.answer_ids)]
|
||||||
|
except Exception,err:
|
||||||
|
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})
|
self.context.update({'submission':submission})
|
||||||
|
|
||||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||||
|
|
||||||
payload = {'xml': xmlstr,
|
payload = {'xml': xmlstr,
|
||||||
### Question: Is this correct/what we want? Shouldn't this be a json.dumps?
|
'edX_cmd' : 'get_score',
|
||||||
'LONCAPA_student_response': ''.join(submission),
|
'edX_student_response': json.dumps(submission),
|
||||||
'LONCAPA_correct_answer': self.tests,
|
'edX_tests': self.tests,
|
||||||
'processor' : self.code,
|
'processor' : self.code,
|
||||||
}
|
}
|
||||||
|
|
||||||
# call external server; TODO: get URL from settings.py
|
r = requests.post(self.url,data=payload) # call external server
|
||||||
r = requests.post("http://eecs1.mit.edu:8889/pyloncapa",data=payload)
|
|
||||||
|
|
||||||
|
if settings.DEBUG: log.info('response = %s' % r.text)
|
||||||
rxml = etree.fromstring(r.text) # response is XML; prase it
|
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||||
ad = rxml.find('awarddetail').text
|
ad = rxml.find('awarddetail').text
|
||||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||||
@@ -520,15 +567,32 @@ class ExternalResponse(GenericResponse):
|
|||||||
# self.context['correct'] = ['correct','correct']
|
# self.context['correct'] = ['correct','correct']
|
||||||
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
|
correct_map = dict(zip(sorted(self.answer_ids), self.context['correct']))
|
||||||
|
|
||||||
# TODO: separate message for each answer_id?
|
# store message in correct_map
|
||||||
correct_map['msg'] = rxml.find('message').text.replace(' ',' ') # store message in correct_map
|
correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace(' ',' ')
|
||||||
|
|
||||||
return correct_map
|
return correct_map
|
||||||
|
|
||||||
def get_answers(self):
|
def get_answers(self):
|
||||||
# Since this is explicitly specified in the problem, this will
|
'''
|
||||||
# be handled by capa_problem
|
Use external server to get expected answers
|
||||||
return {}
|
'''
|
||||||
|
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||||
|
|
||||||
|
payload = {'xml': xmlstr,
|
||||||
|
'edX_cmd' : 'get_answers',
|
||||||
|
'edX_tests': self.tests,
|
||||||
|
'processor' : self.code,
|
||||||
|
}
|
||||||
|
|
||||||
|
r = requests.post(self.url,data=payload) # call external server
|
||||||
|
|
||||||
|
if settings.DEBUG: log.info('response = %s' % r.text)
|
||||||
|
rxml = etree.fromstring(r.text) # response is XML; prase it
|
||||||
|
exans = json.loads(rxml.find('expected').text)
|
||||||
|
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)))
|
||||||
|
raise Exception,'Short response from external server'
|
||||||
|
return dict(zip(self.answer_ids,exans))
|
||||||
|
|
||||||
class StudentInputError(Exception):
|
class StudentInputError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -351,8 +351,15 @@ class Module(XModule):
|
|||||||
|
|
||||||
self.tracker('save_problem_check', event_info)
|
self.tracker('save_problem_check', event_info)
|
||||||
|
|
||||||
|
try:
|
||||||
|
html = self.get_problem_html(encapsulate=False)
|
||||||
|
except Exception,err:
|
||||||
|
log.error('failed to generate html, error %s' % err)
|
||||||
|
raise Exception,err
|
||||||
|
|
||||||
return json.dumps({'success': success,
|
return json.dumps({'success': success,
|
||||||
'contents': self.get_problem_html(encapsulate=False)})
|
'contents': html,
|
||||||
|
})
|
||||||
|
|
||||||
def save_problem(self, get):
|
def save_problem(self, get):
|
||||||
event_info = dict()
|
event_info = dict()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<section class="text-input">
|
<section class="text-input">
|
||||||
<textarea rows="30" cols="80" name="input_${id}" id="input_${id}">${value|h}</textarea>
|
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}">${value|h}</textarea>
|
||||||
|
|
||||||
<span id="answer_${id}"></span>
|
<span id="answer_${id}"></span>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user