From 093ac9d1012dd676a0516b835dd986adea9a82f4 Mon Sep 17 00:00:00 2001 From: kimth Date: Mon, 16 Jul 2012 16:19:35 -0400 Subject: [PATCH] CorrectMap in LMS keeps track of problems as being queued --- common/lib/capa/capa/capa_problem.py | 17 ++++++-- common/lib/capa/capa/correctmap.py | 12 +++++- common/lib/capa/capa/responsetypes.py | 45 +++++++++++++--------- common/lib/xmodule/xmodule/capa_module.py | 12 +++--- lms/djangoapps/courseware/module_render.py | 11 ++++-- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 152ede5b99..00cfc8120f 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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. diff --git a/common/lib/capa/capa/correctmap.py b/common/lib/capa/capa/correctmap.py index 786b2f5e2d..c0426caffb 100644 --- a/common/lib/capa/capa/correctmap.py +++ b/common/lib/capa/capa/correctmap.py @@ -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 diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index c63e8ec9fb..8c31972f70 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -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 #----------------------------------------------------------------------------- diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 4e41b73b94..ba72ad1446 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -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) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index a0281626ba..b2810e8759 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -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: