Commit 093ac9d1 by kimth

CorrectMap in LMS keeps track of problems as being queued

parent 838a8624
......@@ -179,21 +179,32 @@ class LoncapaProblem(object):
return {'score': correct,
'total': self.get_max_score()}
def update_score(self, score_msg):
def update_score(self, score_msg, queuekey):
'''
Deliver grading response (e.g. from async code checking) to
the specific ResponseType
the specific ResponseType that requested grading
Returns an updated CorrectMap
'''
oldcmap = self.correct_map
newcmap = CorrectMap()
for responder in self.responders.values():
if hasattr(responder,'update_score'): # TODO: Is this the best way to target 'update_score' of CodeResponse?
results = responder.update_score(score_msg)
results = responder.update_score(score_msg, oldcmap, queuekey)
newcmap.update(results)
self.correct_map = newcmap
return newcmap
def is_queued(self):
'''
Returns True if any part of the problem has been submitted to an external queue
'''
queued = False
for answer_id in self.correct_map:
if self.correct_map.is_queued(answer_id):
queued = True
return queued
def grade_answers(self, answers):
'''
Grade student responses. Called by capa_module.check_problem.
......
......@@ -14,6 +14,7 @@ class CorrectMap(object):
- msg : string (may have HTML) giving extra message response (displayed below textline or textbox)
- hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg)
- hintmode : one of (None,'on_request','always') criteria for displaying hint
- queuekey : a random integer for xqueue_callback verification
Behaves as a dict.
'''
......@@ -29,13 +30,14 @@ class CorrectMap(object):
def __iter__(self):
return self.cmap.__iter__()
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None):
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuekey=None):
if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints,
'msg': msg,
'hint' : hint,
'hintmode' : hintmode,
'queuekey' : queuekey,
}
def __repr__(self):
......@@ -63,6 +65,14 @@ class CorrectMap(object):
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct'
return None
def is_queued(self,answer_id):
if answer_id in self.cmap: return self.cmap[answer_id]['queuekey'] is not None
return None
def is_right_queuekey(self, answer_id, test_key):
if answer_id in self.cmap: return self.cmap[answer_id]['queuekey'] == test_key
return None
def get_npoints(self,answer_id):
if self.is_correct(answer_id):
npoints = self.cmap[answer_id].get('npoints',1) # default to 1 point if correct
......
......@@ -18,7 +18,6 @@ import re
import requests
import traceback
import abc
import time
# specific library imports
from calc import evaluator, UndefinedVariable
......@@ -709,7 +708,6 @@ class CodeResponse(LoncapaResponse):
self.url = xml.get('url', "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:
......@@ -727,7 +725,7 @@ class CodeResponse(LoncapaResponse):
def get_score(self, student_answers):
idset = sorted(self.answer_ids)
try:
submission = [student_answers[k] for k in idset]
except Exception as err:
......@@ -737,12 +735,16 @@ class CodeResponse(LoncapaResponse):
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()
r, queuekey = self._send_to_queue(extra_payload) # TODO: Perform checks on the xqueue response
def update_score(self, score_msg):
# Non-null CorrectMap['queuekey'] indicates that the problem has been submitted
cmap = CorrectMap()
for answer_id in idset:
cmap.set(answer_id, queuekey=queuekey)
return cmap
def update_score(self, score_msg, oldcmap, queuekey):
# Parse 'score_msg' as XML
try:
rxml = etree.fromstring(score_msg)
......@@ -752,7 +754,6 @@ class CodeResponse(LoncapaResponse):
# 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',
......@@ -761,13 +762,17 @@ class CodeResponse(LoncapaResponse):
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)
# Replace 'oldcmap' with new grading results if queuekey matches
# If queuekey does not match, we keep waiting for the score_msg that will match
for answer_id in idset:
if oldcmap.is_right_queuekey(answer_id, queuekey):
idx = idset.index(answer_id)
msg = rxml.find('message').text.replace(' ',' ') if idx==0 else None
oldcmap.set(answer_id, self.context['correct'][idx], msg=msg)
else: # Queuekey does not match
log.debug('CodeResponse: queuekey %d does not match for answer_id=%s.' % (queuekey, answer_id))
return cmap
return oldcmap
# 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.
......@@ -794,10 +799,12 @@ class CodeResponse(LoncapaResponse):
# Prepare payload
xmlstr = etree.tostring(self.xml, pretty_print=True)
header = { 'return_url': self.system.xqueue_callback_url }
# header.update({'timestamp': time.time()})
random.seed()
header.update({'key': random.randint(0,2**32-1)})
payload = {'xqueue_header': json.dumps(header), # 'xqueue_header' should eventually be derived from xqueue.queue_common.HEADER_TAG or something similar
queuekey = random.randint(0,2**32-1)
header.update({'queuekey': queuekey})
payload = {'xqueue_header': json.dumps(header), # TODO: 'xqueue_header' should eventually be derived from config file
'xml': xmlstr,
'edX_cmd': 'get_score',
'edX_tests': self.tests,
......@@ -813,7 +820,7 @@ class CodeResponse(LoncapaResponse):
log.error(msg)
raise Exception(msg)
return r
return r, queuekey
#-----------------------------------------------------------------------------
......
......@@ -332,8 +332,9 @@ class CapaModule(XModule):
No ajax return is needed. Return empty dict.
"""
queuekey = get['queuekey']
score_msg = get['response']
self.lcp.update_score(score_msg)
self.lcp.update_score(score_msg, queuekey)
return dict() # No AJAX return is needed
......@@ -433,10 +434,11 @@ class CapaModule(XModule):
if not correct_map.is_correct(answer_id):
success = 'incorrect'
# log this in the track_function
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
self.system.track_function('save_problem_check', event_info)
# log this in the track_function, ONLY if a full grading has been performed (e.g. not queueing)
if not self.lcp.is_queued():
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
self.system.track_function('save_problem_check', event_info)
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
......
......@@ -33,6 +33,8 @@ class I4xSystem(object):
Create a closure around the system environment.
ajax_url - the url where ajax calls to the encapsulating module go.
xqueue_callback_url - the url where external queueing system (e.g. for grading)
returns its response
track_function - function of (event_type, event), intended for logging
or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different
......@@ -324,7 +326,7 @@ def add_histogram(module):
module.get_html = get_html
return module
# THK: TEMPORARY BYPASS OF AUTH!
# TODO: TEMPORARY BYPASS OF AUTH!
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
@csrf_exempt
......@@ -336,9 +338,6 @@ def xqueue_callback(request, username, id, dispatch):
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)
......@@ -354,6 +353,10 @@ def xqueue_callback(request, username, id, dispatch):
oldgrade = instance_module.grade
old_instance_state = instance_module.state
# Transfer 'queuekey' from xqueue response header to 'get'. This is required to
# use the interface defined by 'handle_ajax'
get.update({'queuekey': header['queuekey']})
# We go through the "AJAX" path
# So far, the only dispatch from xqueue will be 'score_update'
try:
......
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