Commit 231b8c6a by David Ormsbee

Merge pull request #548 from MITx/kimth/limit-queue-dos

Kimth/limit queue dos
parents 18127cf9 377b09e1
...@@ -14,6 +14,8 @@ This is used by capa_module. ...@@ -14,6 +14,8 @@ This is used by capa_module.
from __future__ import division from __future__ import division
from datetime import datetime
import json
import logging import logging
import math import math
import numpy import numpy
...@@ -32,6 +34,7 @@ from correctmap import CorrectMap ...@@ -32,6 +34,7 @@ from correctmap import CorrectMap
import eia import eia
import inputtypes import inputtypes
from util import contextualize_text, convert_files_to_filenames from util import contextualize_text, convert_files_to_filenames
import xqueue_interface
# to be replaced with auto-registering # to be replaced with auto-registering
import responsetypes import responsetypes
...@@ -202,11 +205,24 @@ class LoncapaProblem(object): ...@@ -202,11 +205,24 @@ class LoncapaProblem(object):
''' '''
Returns True if any part of the problem has been submitted to an external queue Returns True if any part of the problem has been submitted to an external queue
''' '''
queued = False return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
for answer_id in self.correct_map:
if self.correct_map.is_queued(answer_id):
queued = True def get_recentmost_queuetime(self):
return queued '''
Returns a DateTime object that represents the timestamp of the most recent queueing request, or None if not queued
'''
if not self.is_queued():
return None
# Get a list of timestamps of all queueing requests, then convert it to a DateTime object
queuetime_strs = [self.correct_map.get_queuetime_str(answer_id)
for answer_id in self.correct_map
if self.correct_map.is_queued(answer_id)]
queuetimes = [datetime.strptime(qt_str, xqueue_interface.dateformat) for qt_str in queuetime_strs]
return max(queuetimes)
def grade_answers(self, answers): def grade_answers(self, answers):
''' '''
......
...@@ -15,7 +15,8 @@ class CorrectMap(object): ...@@ -15,7 +15,8 @@ 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 - queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump
of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued
Behaves as a dict. Behaves as a dict.
''' '''
...@@ -31,14 +32,15 @@ class CorrectMap(object): ...@@ -31,14 +32,15 @@ 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, queuekey=None): # See the documentation for 'set_dict' for the use of kwargs
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs):
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, 'queuestate': queuestate,
} }
def __repr__(self): def __repr__(self):
...@@ -52,25 +54,39 @@ class CorrectMap(object): ...@@ -52,25 +54,39 @@ class CorrectMap(object):
def set_dict(self, correct_map): def set_dict(self, correct_map):
''' '''
set internal dict to provided correct_map dict Set internal dict of CorrectMap to provided correct_map dict
for graceful migration, if correct_map is a one-level dict, then convert it to the new
dict of dicts format. correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that
when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict
not coincide with the newest CorrectMap format as defined by self.set.
For graceful migration, feed the contents of each correct map to self.set, rather than
making a direct copy of the given correct_map dict. This way, the common keys between
the incoming correct_map dict and the new CorrectMap instance will be written, while
mismatched keys will be gracefully ignored.
Special migration case:
If correct_map is a one-level dict, then convert it to the new dict of dicts format.
''' '''
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
self.__init__() # empty current dict self.__init__() # empty current dict
for k in correct_map: self.set(k, correct_map[k]) # create new dict entries for k in correct_map: self.set(k, correct_map[k]) # create new dict entries
else: else:
self.cmap = correct_map self.__init__()
for k in correct_map: self.set(k, **correct_map[k])
def is_correct(self, answer_id): def is_correct(self, answer_id):
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): def is_queued(self, answer_id):
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] is not None return answer_id in self.cmap and self.cmap[answer_id]['queuestate'] is not None
def is_right_queuekey(self, answer_id, test_key): def is_right_queuekey(self, answer_id, test_key):
return answer_id in self.cmap and self.cmap[answer_id]['queuekey'] == test_key return self.is_queued(answer_id) and self.cmap[answer_id]['queuestate']['key'] == test_key
def get_queuetime_str(self, answer_id):
return self.cmap[answer_id]['queuestate']['time']
def get_npoints(self, answer_id): def get_npoints(self, answer_id):
npoints = self.get_property(answer_id, 'npoints') npoints = self.get_property(answer_id, 'npoints')
......
...@@ -351,7 +351,7 @@ def filesubmission(element, value, status, render_template, msg=''): ...@@ -351,7 +351,7 @@ def filesubmission(element, value, status, render_template, msg=''):
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued' status = 'queued'
queue_len = msg queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len msg = 'Submitted to grader.'
context = { 'id': eid, 'state': status, 'msg': msg, 'value': value, context = { 'id': eid, 'state': status, 'msg': msg, 'value': value,
'queue_len': queue_len, 'allowed_files': allowed_files, 'queue_len': queue_len, 'allowed_files': allowed_files,
...@@ -384,7 +384,7 @@ def textbox(element, value, status, render_template, msg=''): ...@@ -384,7 +384,7 @@ def textbox(element, value, status, render_template, msg=''):
if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue if status == 'incomplete': # Flag indicating that the problem has been queued, 'msg' is length of queue
status = 'queued' status = 'queued'
queue_len = msg queue_len = msg
msg = 'Submitted to grader. (Queue length: %s)' % queue_len msg = 'Submitted to grader.'
# For CodeMirror # For CodeMirror
mode = element.get('mode','python') mode = element.get('mode','python')
......
...@@ -26,6 +26,7 @@ import xml.sax.saxutils as saxutils ...@@ -26,6 +26,7 @@ import xml.sax.saxutils as saxutils
# specific library imports # specific library imports
from calc import evaluator, UndefinedVariable from calc import evaluator, UndefinedVariable
from correctmap import CorrectMap from correctmap import CorrectMap
from datetime import datetime
from util import * from util import *
from lxml import etree from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
...@@ -1026,7 +1027,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1026,7 +1027,7 @@ class CodeResponse(LoncapaResponse):
TODO: Determines whether in synchronous or asynchronous (queued) mode TODO: Determines whether in synchronous or asynchronous (queued) mode
''' '''
xml = self.xml xml = self.xml
self.url = xml.get('url', None) # XML can override external resource (grader/queue) URL self.url = xml.get('url', None) # TODO: XML can override external resource (grader/queue) URL
self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename']) self.queue_name = xml.get('queuename', self.system.xqueue['default_queuename'])
# VS[compat]: # VS[compat]:
...@@ -1128,7 +1129,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1128,7 +1129,7 @@ class CodeResponse(LoncapaResponse):
xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'], xheader = xqueue_interface.make_xheader(lms_callback_url=self.system.xqueue['callback_url'],
lms_key=queuekey, lms_key=queuekey,
queue_name=self.queue_name) queue_name=self.queue_name)
# Generate body # Generate body
if is_list_of_files(submission): if is_list_of_files(submission):
self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here self.context.update({'submission': queuekey}) # For tracking. TODO: May want to record something else here
...@@ -1148,16 +1149,22 @@ class CodeResponse(LoncapaResponse): ...@@ -1148,16 +1149,22 @@ class CodeResponse(LoncapaResponse):
(error, msg) = qinterface.send_to_queue(header=xheader, (error, msg) = qinterface.send_to_queue(header=xheader,
body=json.dumps(contents)) body=json.dumps(contents))
# State associated with the queueing request
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
queuestate = {'key': queuekey,
'time': qtime,
}
cmap = CorrectMap() cmap = CorrectMap()
if error: if error:
cmap.set(self.answer_id, queuekey=None, cmap.set(self.answer_id, queuestate=None,
msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg) msg='Unable to deliver your submission to grader. (Reason: %s.) Please try again later.' % msg)
else: else:
# Queueing mechanism flags: # Queueing mechanism flags:
# 1) Backend: Non-null CorrectMap['queuekey'] indicates that the problem has been queued # 1) Backend: Non-null CorrectMap['queuestate'] indicates that the problem has been queued
# 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox # 2) Frontend: correctness='incomplete' eventually trickles down through inputtypes.textbox
# and .filesubmission to inform the browser to poll the LMS # and .filesubmission to inform the browser to poll the LMS
cmap.set(self.answer_id, queuekey=queuekey, correctness='incomplete', msg=msg) cmap.set(self.answer_id, queuestate=queuestate, correctness='incomplete', msg=msg)
return cmap return cmap
...@@ -1180,7 +1187,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1180,7 +1187,7 @@ class CodeResponse(LoncapaResponse):
points = 0 points = 0
elif points > self.maxpoints[self.answer_id]: elif points > self.maxpoints[self.answer_id]:
points = self.maxpoints[self.answer_id] points = self.maxpoints[self.answer_id]
oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuekey=None) # Queuekey is consumed oldcmap.set(self.answer_id, npoints=points, correctness=correctness, msg=msg.replace(' ', ' '), queuestate=None) # Queuestate is consumed
else: else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id)) log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' % (queuekey, self.answer_id))
...@@ -1197,7 +1204,7 @@ class CodeResponse(LoncapaResponse): ...@@ -1197,7 +1204,7 @@ class CodeResponse(LoncapaResponse):
''' '''
Grader reply is a JSON-dump of the following dict Grader reply is a JSON-dump of the following dict
{ 'correct': True/False, { 'correct': True/False,
'score': # TODO -- Partial grading 'score': Numeric value (floating point is okay) to assign to answer
'msg': grader_msg } 'msg': grader_msg }
Returns (valid_score_msg, correct, score, msg): Returns (valid_score_msg, correct, score, msg):
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<span class="processing" id="status_${id}"></span> <span class="processing" id="status_${id}"></span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
<span class="debug">(${state})</span> <span style="display:none;" class="debug">(${state})</span>
<br/> <br/>
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
<br/> <br/>
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<br/> <br/>
<span class="debug">(${state})</span> <span style="display:none;" class="debug">(${state})</span>
<br/> <br/>
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
<br/> <br/>
......
...@@ -9,7 +9,7 @@ import time ...@@ -9,7 +9,7 @@ import time
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
dateformat = '%Y%m%d%H%M%S'
def make_hashkey(seed=None): def make_hashkey(seed=None):
''' '''
......
...@@ -462,6 +462,15 @@ class CapaModule(XModule): ...@@ -462,6 +462,15 @@ class CapaModule(XModule):
self.system.track_function('save_problem_check_fail', event_info) self.system.track_function('save_problem_check_fail', event_info)
raise NotFoundError('Problem must be reset before it can be checked again') raise NotFoundError('Problem must be reset before it can be checked again')
# Problem queued. Students must wait a specified waittime before they are allowed to submit
if self.lcp.is_queued():
current_time = datetime.datetime.now()
prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime']
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try: try:
old_state = self.lcp.get_state() old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id lcp_id = self.lcp.problem_id
......
...@@ -19,6 +19,8 @@ import capa.calc as calc ...@@ -19,6 +19,8 @@ import capa.calc as calc
import capa.capa_problem as lcp import capa.capa_problem as lcp
from capa.correctmap import CorrectMap from capa.correctmap import CorrectMap
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from capa.xqueue_interface import dateformat
from datetime import datetime
from xmodule import graders, x_module from xmodule import graders, x_module
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
from xmodule.graders import Score, aggregate_scores from xmodule.graders import Score, aggregate_scores
...@@ -35,7 +37,7 @@ i4xs = ModuleSystem( ...@@ -35,7 +37,7 @@ i4xs = ModuleSystem(
user=Mock(), user=Mock(),
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"), filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))+"/test_files"),
debug=True, debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue'}, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules") node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules")
) )
...@@ -283,70 +285,139 @@ class CodeResponseTest(unittest.TestCase): ...@@ -283,70 +285,139 @@ class CodeResponseTest(unittest.TestCase):
''' '''
Test CodeResponse Test CodeResponse
''' '''
def test_update_score(self): @staticmethod
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" def make_queuestate(key, time):
test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=i4xs) timestr = datetime.strftime(time, dateformat)
return {'key': key, 'time': timestr}
# CodeResponse requires internal CorrectMap state. Build it now in the 'queued' state
old_cmap = CorrectMap() def test_is_queued(self):
answer_ids = sorted(test_lcp.get_question_answers().keys()) '''
numAnswers = len(answer_ids) Simple test of whether LoncapaProblem knows when it's been queued
for i in range(numAnswers): '''
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuekey=1000 + i)) problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml")
with open(problem_file) as input_file:
test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs)
answer_ids = sorted(test_lcp.get_question_answers())
# CodeResponse requires internal CorrectMap state. Build it now in the unqueued state
cmap = CorrectMap()
for answer_id in answer_ids:
cmap.update(CorrectMap(answer_id=answer_id, queuestate=None))
test_lcp.correct_map.update(cmap)
self.assertEquals(test_lcp.is_queued(), False)
# Now we queue the LCP
cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids):
queuestate = CodeResponseTest.make_queuestate(i, datetime.now())
cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
test_lcp.correct_map.update(cmap)
self.assertEquals(test_lcp.is_queued(), True)
# TODO: Message format inherited from ExternalResponse def test_update_score(self):
#correct_score_msg = "<edxgrade><awarddetail>EXACT_ANS</awarddetail><message>MESSAGE</message></edxgrade>" '''
#incorrect_score_msg = "<edxgrade><awarddetail>WRONG_FORMAT</awarddetail><message>MESSAGE</message></edxgrade>" Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem
'''
# New message format common to external graders problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml")
correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'}) with open(problem_file) as input_file:
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'}) test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs)
xserver_msgs = {'correct': correct_score_msg, answer_ids = sorted(test_lcp.get_question_answers())
'incorrect': incorrect_score_msg,
} # CodeResponse requires internal CorrectMap state. Build it now in the queued state
old_cmap = CorrectMap()
# Incorrect queuekey, state should not be updated for i, answer_id in enumerate(answer_ids):
for correctness in ['correct', 'incorrect']: queuekey = 1000 + i
test_lcp.correct_map = CorrectMap() queuestate = CodeResponseTest.make_queuestate(1000+i, datetime.now())
test_lcp.correct_map.update(old_cmap) # Deep copy old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
test_lcp.update_score(xserver_msgs[correctness], queuekey=0) # Message format common to external graders
self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison correct_score_msg = json.dumps({'correct':True, 'score':1, 'msg':'MESSAGE'})
incorrect_score_msg = json.dumps({'correct':False, 'score':0, 'msg':'MESSAGE'})
for i in range(numAnswers):
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[i])) # Should be still queued, since message undelivered xserver_msgs = {'correct': correct_score_msg,
'incorrect': incorrect_score_msg,}
# Correct queuekey, state should be updated
for correctness in ['correct', 'incorrect']: # Incorrect queuekey, state should not be updated
for i in range(numAnswers): # Target specific answer_id's for correctness in ['correct', 'incorrect']:
test_lcp.correct_map = CorrectMap() test_lcp.correct_map = CorrectMap()
test_lcp.correct_map.update(old_cmap) test_lcp.correct_map.update(old_cmap) # Deep copy
new_cmap = CorrectMap() test_lcp.update_score(xserver_msgs[correctness], queuekey=0)
new_cmap.update(old_cmap) self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison
npoints = 1 if correctness=='correct' else 0
new_cmap.set(answer_id=answer_ids[i], npoints=npoints, correctness=correctness, msg='MESSAGE', queuekey=None) for answer_id in answer_ids:
self.assertTrue(test_lcp.correct_map.is_queued(answer_id)) # Should be still queued, since message undelivered
test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) # Correct queuekey, state should be updated
for correctness in ['correct', 'incorrect']:
for j in range(numAnswers): for i, answer_id in enumerate(answer_ids):
if j == i: test_lcp.correct_map = CorrectMap()
self.assertFalse(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be dequeued, message delivered test_lcp.correct_map.update(old_cmap)
else:
self.assertTrue(test_lcp.correct_map.is_queued(answer_ids[j])) # Should be queued, message undelivered new_cmap = CorrectMap()
new_cmap.update(old_cmap)
def test_convert_files_to_filenames(self): npoints = 1 if correctness=='correct' else 0
problem_file = os.path.dirname(__file__) + "/test_files/coderesponse.xml" new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg='MESSAGE', queuestate=None)
fp = open(problem_file)
answers_with_file = {'1_2_1': 'String-based answer', test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i)
'1_3_1': ['answer1', 'answer2', 'answer3'], self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict())
'1_4_1': [fp, fp]}
answers_converted = convert_files_to_filenames(answers_with_file) for j, test_id in enumerate(answer_ids):
self.assertEquals(answers_converted['1_2_1'], 'String-based answer') if j == i:
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) self.assertFalse(test_lcp.correct_map.is_queued(test_id)) # Should be dequeued, message delivered
self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) else:
self.assertTrue(test_lcp.correct_map.is_queued(test_id)) # Should be queued, message undelivered
def test_recentmost_queuetime(self):
'''
Test whether the LoncapaProblem knows about the time of queue requests
'''
problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml")
with open(problem_file) as input_file:
test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=i4xs)
answer_ids = sorted(test_lcp.get_question_answers())
# CodeResponse requires internal CorrectMap state. Build it now in the unqueued state
cmap = CorrectMap()
for answer_id in answer_ids:
cmap.update(CorrectMap(answer_id=answer_id, queuestate=None))
test_lcp.correct_map.update(cmap)
self.assertEquals(test_lcp.get_recentmost_queuetime(), None)
# CodeResponse requires internal CorrectMap state. Build it now in the queued state
cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i
latest_timestamp = datetime.now()
queuestate = CodeResponseTest.make_queuestate(1000+i, latest_timestamp)
cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate))
test_lcp.correct_map.update(cmap)
# Queue state only tracks up to second
latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp, dateformat), dateformat)
self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp)
def test_convert_files_to_filenames(self):
'''
Test whether file objects are converted to filenames without altering other structures
'''
problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml")
with open(problem_file) as fp:
answers_with_file = {'1_2_1': 'String-based answer',
'1_3_1': ['answer1', 'answer2', 'answer3'],
'1_4_1': [fp, fp]}
answers_converted = convert_files_to_filenames(answers_with_file)
self.assertEquals(answers_converted['1_2_1'], 'String-based answer')
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name])
class ChoiceResponseTest(unittest.TestCase): class ChoiceResponseTest(unittest.TestCase):
......
...@@ -9,91 +9,23 @@ ...@@ -9,91 +9,23 @@
Write a program to compute the square of a number Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate"> <coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/> <textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[ <codeparam>
initial_display = """ <initial_display>def square(x):</initial_display>
def square(n): <answer_display>answer</answer_display>
""" <grader_payload>grader stuff</grader_payload>
</codeparam>
answer = """
def square(n):
return n**2
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testSquare(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: square(%d)'%n
return str(square(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testSquare(0))
elif test == 2: f.write(testSquare(1))
else: f.write(testSquare())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse> </coderesponse>
</text> </text>
<text> <text>
Write a program to compute the cube of a number Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate"> <coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/> <textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[ <codeparam>
initial_display = """ <initial_display>def square(x):</initial_display>
def cube(n): <answer_display>answer</answer_display>
""" <grader_payload>grader stuff</grader_payload>
</codeparam>
answer = """
def cube(n):
return n**3
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testCube(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: cube(%d)'%n
return str(cube(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testCube(0))
elif test == 2: f.write(testCube(1))
else: f.write(testCube())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse> </coderesponse>
</text> </text>
......
<problem>
<text>
<h2>Code response</h2>
<p>
</p>
<text>
Write a program to compute the square of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[
initial_display = """
def square(n):
"""
answer = """
def square(n):
return n**2
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testSquare(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: square(%d)'%n
return str(square(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testSquare(0))
elif test == 2: f.write(testSquare(1))
else: f.write(testSquare())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse>
</text>
<text>
Write a program to compute the cube of a number
<coderesponse tests="repeat:2,generate">
<textbox rows="10" cols="70" mode="python"/>
<answer><![CDATA[
initial_display = """
def cube(n):
"""
answer = """
def cube(n):
return n**3
"""
preamble = """
import sys, time
"""
test_program = """
import random
import operator
def testCube(n = None):
if n is None:
n = random.randint(2, 20)
print 'Test is: cube(%d)'%n
return str(cube(n))
def main():
f = os.fdopen(3,'w')
test = int(sys.argv[1])
rndlist = map(int,os.getenv('rndlist').split(','))
random.seed(rndlist[0])
if test == 1: f.write(testCube(0))
elif test == 2: f.write(testCube(1))
else: f.write(testCube())
f.close()
main()
sys.exit(0)
"""
]]>
</answer>
</coderesponse>
</text>
</text>
</problem>
...@@ -220,7 +220,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi ...@@ -220,7 +220,9 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
xqueue = {'interface': xqueue_interface, xqueue = {'interface': xqueue_interface,
'callback_url': xqueue_callback_url, 'callback_url': xqueue_callback_url,
'default_queuename': xqueue_default_queuename.replace(' ', '_')} 'default_queuename': xqueue_default_queuename.replace(' ', '_'),
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
}
def inner_get_module(location): def inner_get_module(location):
""" """
......
...@@ -86,6 +86,9 @@ DEFAULT_GROUPS = [] ...@@ -86,6 +86,9 @@ DEFAULT_GROUPS = []
# If this is true, random scores will be generated for the purpose of debugging the profile graphs # If this is true, random scores will be generated for the purpose of debugging the profile graphs
GENERATE_PROFILE_SCORES = False GENERATE_PROFILE_SCORES = False
# Used with XQueue
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
############################# SET PATH INFORMATION ############################# ############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms
REPO_ROOT = PROJECT_ROOT.dirname() REPO_ROOT = PROJECT_ROOT.dirname()
......
...@@ -58,7 +58,7 @@ XQUEUE_INTERFACE = { ...@@ -58,7 +58,7 @@ XQUEUE_INTERFACE = {
}, },
"basic_auth": ('anant', 'agarwal'), "basic_auth": ('anant', 'agarwal'),
} }
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
# TODO (cpennington): We need to figure out how envs/test.py can inject things # TODO (cpennington): We need to figure out how envs/test.py can inject things
# into common.py so that we don't have to repeat this sort of thing # into common.py so that we don't have to repeat this sort of thing
......
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