CorrectMap in LMS keeps track of problems as being queued
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user