Commit 1aa05fd4 by ichuang

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.
parent b1acec7c
...@@ -253,16 +253,19 @@ def textbox(element, value, status, msg=''): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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('&nbsp;','&#160;') # store message in correct_map correct_map['msg_%s' % self.answer_ids[0]] = rxml.find('message').text.replace('&nbsp;','&#160;')
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): ...@@ -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()
......
<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>
......
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