Commit 093ac9d1 by kimth

CorrectMap in LMS keeps track of problems as being queued

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