Merge pull request #180 from MITx/kimth/lms-coderesponse
CodeResponse for external/queued grading of student code
This commit is contained in:
@@ -179,6 +179,15 @@ class LoncapaProblem(object):
|
||||
return {'score': correct,
|
||||
'total': self.get_max_score()}
|
||||
|
||||
def update_score(self, score_msg):
|
||||
newcmap = CorrectMap()
|
||||
for responder in self.responders.values():
|
||||
if hasattr(responder,'update_score'): # Is this the best way to implement 'update_score' for CodeResponse?
|
||||
results = responder.update_score(score_msg)
|
||||
newcmap.update(results)
|
||||
self.correct_map = newcmap
|
||||
return newcmap
|
||||
|
||||
def grade_answers(self, answers):
|
||||
'''
|
||||
Grade student responses. Called by capa_module.check_problem.
|
||||
|
||||
@@ -18,6 +18,7 @@ import re
|
||||
import requests
|
||||
import traceback
|
||||
import abc
|
||||
import time
|
||||
|
||||
# specific library imports
|
||||
from calc import evaluator, UndefinedVariable
|
||||
@@ -693,6 +694,124 @@ class SymbolicResponse(CustomResponse):
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class CodeResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade student code using an external server
|
||||
'''
|
||||
|
||||
response_tag = 'coderesponse'
|
||||
allowed_inputfields = ['textline', 'textbox']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
self.url = xml.get('url') or "http://ec2-50-16-59-149.compute-1.amazonaws.com/xqueue/submit/" # FIXME -- hardcoded url
|
||||
|
||||
answer = xml.find('answer')
|
||||
if answer is not None:
|
||||
answer_src = answer.get('src')
|
||||
if answer_src is not None:
|
||||
self.code = self.system.filesystem.open('src/'+answer_src).read()
|
||||
else:
|
||||
self.code = answer.text
|
||||
else: # no <answer> stanza; get code from <script>
|
||||
self.code = self.context['script_code']
|
||||
if not self.code:
|
||||
msg = '%s: Missing answer script code for externalresponse' % unicode(self)
|
||||
msg += "\nSee XML source line %s" % getattr(self.xml,'sourceline','<unavailable>')
|
||||
raise LoncapaProblemError(msg)
|
||||
|
||||
self.tests = xml.get('tests')
|
||||
|
||||
def get_score(self, student_answers):
|
||||
idset = sorted(self.answer_ids)
|
||||
|
||||
try:
|
||||
submission = [student_answers[k] for k in idset]
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: cannot get student answer for %s; student_answers=%s' % (err, self.answer_ids, student_answers))
|
||||
raise Exception(err)
|
||||
|
||||
self.context.update({'submission': submission})
|
||||
extra_payload = {'edX_student_response': json.dumps(submission)}
|
||||
|
||||
# Should do something -- like update the problem state -- based on the queue response
|
||||
r = self._send_to_queue(extra_payload)
|
||||
|
||||
return CorrectMap()
|
||||
|
||||
def update_score(self, score_msg):
|
||||
# Parse 'score_msg' as XML
|
||||
try:
|
||||
rxml = etree.fromstring(score_msg)
|
||||
except Exception as err:
|
||||
msg = 'Error in CodeResponse %s: cannot parse response from xworker r.text=%s' % (err, score_msg)
|
||||
raise Exception(err)
|
||||
|
||||
# The following process is lifted directly from ExternalResponse
|
||||
idset = sorted(self.answer_ids)
|
||||
cmap = CorrectMap()
|
||||
ad = rxml.find('awarddetail').text
|
||||
admap = {'EXACT_ANS':'correct', # TODO: handle other loncapa responses
|
||||
'WRONG_FORMAT': 'incorrect',
|
||||
}
|
||||
self.context['correct'] = ['correct']
|
||||
if ad in admap:
|
||||
self.context['correct'][0] = admap[ad]
|
||||
|
||||
# create CorrectMap
|
||||
for key in idset:
|
||||
idx = idset.index(key)
|
||||
msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None
|
||||
cmap.set(key, self.context['correct'][idx], msg=msg)
|
||||
|
||||
return cmap
|
||||
|
||||
# CodeResponse differentiates from ExternalResponse in the behavior of 'get_answers'. CodeResponse.get_answers
|
||||
# does NOT require a queue submission, and the answer is computed (extracted from problem XML) locally.
|
||||
def get_answers(self):
|
||||
# Extract the CodeResponse answer from XML
|
||||
penv = {}
|
||||
penv['__builtins__'] = globals()['__builtins__']
|
||||
try:
|
||||
exec(self.code,penv,penv)
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: Error in problem reference code' % err)
|
||||
raise Exception(err)
|
||||
try:
|
||||
ans = penv['answer']
|
||||
except Exception as err:
|
||||
log.error('Error in CodeResponse %s: Problem reference code does not define answer in <answer>...</answer>' % err)
|
||||
raise Exception(err)
|
||||
|
||||
anshtml = '<font color="blue"><span class="code-answer"><br/><pre>%s</pre><br/></span></font>' % ans
|
||||
return dict(zip(self.answer_ids,[anshtml]))
|
||||
|
||||
# CodeResponse._send_to_queue implements the same interface as defined for ExternalResponse's 'get_score'
|
||||
def _send_to_queue(self, extra_payload):
|
||||
# Prepare payload
|
||||
xmlstr = etree.tostring(self.xml, pretty_print=True)
|
||||
header = { 'return_url': self.system.xqueue_callback_url }
|
||||
header.update({'timestamp': time.time()})
|
||||
payload = {'xqueue_header': json.dumps(header), # 'xqueue_header' should eventually be derived from xqueue.queue_common.HEADER_TAG or something similar
|
||||
'xml': xmlstr,
|
||||
'edX_cmd': 'get_score',
|
||||
'edX_tests': self.tests,
|
||||
'processor': self.code,
|
||||
}
|
||||
payload.update(extra_payload)
|
||||
|
||||
# Contact queue server
|
||||
try:
|
||||
r = requests.post(self.url, data=payload)
|
||||
except Exception as err:
|
||||
msg = "Error in CodeResponse %s: cannot connect to queue server url=%s" % (err, self.url)
|
||||
log.error(msg)
|
||||
raise Exception(msg)
|
||||
|
||||
return r
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class ExternalResponse(LoncapaResponse):
|
||||
'''
|
||||
Grade the students input using an external server.
|
||||
@@ -1072,5 +1191,5 @@ class ImageResponse(LoncapaResponse):
|
||||
# TEMPORARY: List of all response subclasses
|
||||
# FIXME: To be replaced by auto-registration
|
||||
|
||||
__all__ = [ NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse ]
|
||||
__all__ = [ CodeResponse, NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, TrueFalseResponse, ExternalResponse, ImageResponse, OptionResponse, SymbolicResponse, StringResponse ]
|
||||
|
||||
|
||||
@@ -275,6 +275,7 @@ class CapaModule(XModule):
|
||||
'problem_reset': self.reset_problem,
|
||||
'problem_save': self.save_problem,
|
||||
'problem_show': self.get_answer,
|
||||
'score_update': self.update_score,
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -321,6 +322,12 @@ class CapaModule(XModule):
|
||||
#TODO: Not 404
|
||||
raise self.system.exception404
|
||||
|
||||
def update_score(self, get):
|
||||
score_msg = get['response']
|
||||
self.lcp.update_score(score_msg)
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
|
||||
@@ -28,7 +28,7 @@ class I4xSystem(object):
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, replace_urls,
|
||||
user=None, filestore=None):
|
||||
user=None, filestore=None, xqueue_callback_url=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -48,6 +48,7 @@ class I4xSystem(object):
|
||||
that capa_module can use to fix up the static urls in ajax results.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue_callback_url = xqueue_callback_url
|
||||
self.track_function = track_function
|
||||
self.filestore = filestore
|
||||
self.get_module = get_module
|
||||
@@ -207,6 +208,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
|
||||
# Setup system context for module instance
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
xqueue_callback_url = settings.MITX_ROOT_URL + '/xqueue/' + user.username + '/' + descriptor.location.url() + '/'
|
||||
|
||||
def _get_module(location):
|
||||
(module, _, _, _) = get_module(user, request, location, student_module_cache, position)
|
||||
@@ -218,6 +220,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
system = I4xSystem(track_function=make_track_function(request),
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
xqueue_callback_url=xqueue_callback_url,
|
||||
# TODO (cpennington): Figure out how to share info between systems
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=_get_module,
|
||||
@@ -321,6 +324,53 @@ def add_histogram(module):
|
||||
module.get_html = get_html
|
||||
return module
|
||||
|
||||
# THK: TEMPORARY BYPASS OF AUTH!
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.models import User
|
||||
@csrf_exempt
|
||||
def xqueue_callback(request, username, id, dispatch):
|
||||
# Parse xqueue response
|
||||
get = request.POST.copy()
|
||||
try:
|
||||
header = json.loads(get.pop('xqueue_header')[0]) # 'dict'
|
||||
except Exception as err:
|
||||
msg = "Error in xqueue_callback %s: Invalid return format" % err
|
||||
raise Exception(msg)
|
||||
|
||||
# Should proceed only when the request timestamp is more recent than problem timestamp
|
||||
timestamp = header['timestamp']
|
||||
|
||||
# Retrieve target StudentModule
|
||||
user = User.objects.get(username=username)
|
||||
|
||||
student_module_cache = StudentModuleCache(user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
raise Http404
|
||||
|
||||
oldgrade = instance_module.grade
|
||||
old_instance_state = instance_module.state
|
||||
|
||||
# We go through the "AJAX" path
|
||||
# So far, the only dispatch from xqueue will be 'score_update'
|
||||
try:
|
||||
ajax_return = instance.handle_ajax(dispatch, get) # Can ignore the "ajax" return in 'xqueue_callback'
|
||||
except:
|
||||
log.exception("error processing ajax call")
|
||||
raise
|
||||
|
||||
# Save state back to database
|
||||
instance_module.state = instance.get_instance_state()
|
||||
if instance.get_score():
|
||||
instance_module.grade = instance.get_score()['score']
|
||||
if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
|
||||
instance_module.save()
|
||||
|
||||
return HttpResponse("")
|
||||
|
||||
def modx_dispatch(request, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.
|
||||
|
||||
@@ -339,7 +389,7 @@ def modx_dispatch(request, dispatch=None, id=None):
|
||||
|
||||
student_module_cache = StudentModuleCache(request.user, modulestore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
|
||||
@@ -58,6 +58,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^masquerade/', include('masquerade.urls')),
|
||||
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
|
||||
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
|
||||
url(r'^xqueue/(?P<username>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
|
||||
url(r'^change_setting$', 'student.views.change_setting'),
|
||||
url(r'^s/(?P<template>[^/]*)$', 'static_template_view.views.auth_index'),
|
||||
url(r'^book/(?P<page>[^/]*)$', 'staticbook.views.index'),
|
||||
|
||||
Reference in New Issue
Block a user