Add the ability for input types to have their own state
and add in a handler for ungraded responses via xqueue
This commit is contained in:
@@ -111,6 +111,7 @@ class LoncapaProblem(object):
|
||||
if self.system is None:
|
||||
raise Exception()
|
||||
self.seed = seed
|
||||
self.input_state = None
|
||||
|
||||
if state:
|
||||
if 'seed' in state:
|
||||
@@ -121,11 +122,16 @@ class LoncapaProblem(object):
|
||||
self.correct_map.set_dict(state['correct_map'])
|
||||
if 'done' in state:
|
||||
self.done = state['done']
|
||||
if 'input_state' in state:
|
||||
self.input_state = state['input_state']
|
||||
|
||||
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
|
||||
if not self.seed:
|
||||
self.seed = struct.unpack('i', os.urandom(4))[0]
|
||||
|
||||
if not self.input_state:
|
||||
self.input_state = {}
|
||||
|
||||
# Convert startouttext and endouttext to proper <text></text>
|
||||
problem_text = re.sub("startouttext\s*/", "text", problem_text)
|
||||
problem_text = re.sub("endouttext\s*/", "/text", problem_text)
|
||||
@@ -188,6 +194,7 @@ class LoncapaProblem(object):
|
||||
return {'seed': self.seed,
|
||||
'student_answers': self.student_answers,
|
||||
'correct_map': self.correct_map.get_dict(),
|
||||
'input_state': self.input_state,
|
||||
'done': self.done}
|
||||
|
||||
def get_max_score(self):
|
||||
@@ -237,6 +244,19 @@ class LoncapaProblem(object):
|
||||
self.correct_map.set_dict(cmap.get_dict())
|
||||
return cmap
|
||||
|
||||
def ungraded_response(self, xqueue_msg, queuekey):
|
||||
'''
|
||||
Handle any responses from the xqueue that are not related to grading
|
||||
|
||||
Does not return any value
|
||||
'''
|
||||
# check against each inputtype
|
||||
for the_input in self.inputs.values():
|
||||
# if the input type has an xqueue_response function, pass in the values
|
||||
if hasattr(the_input, 'ungraded_response'):
|
||||
the_input.ungraded_response(xqueue_msg, queuekey)
|
||||
|
||||
|
||||
def is_queued(self):
|
||||
'''
|
||||
Returns True if any part of the problem has been submitted to an external queue
|
||||
@@ -527,11 +547,15 @@ class LoncapaProblem(object):
|
||||
value = ""
|
||||
if self.student_answers and problemid in self.student_answers:
|
||||
value = self.student_answers[problemid]
|
||||
|
||||
if input_id not in self.input_state:
|
||||
self.input_state[input_id] = {}
|
||||
|
||||
# do the rendering
|
||||
state = {'value': value,
|
||||
'status': status,
|
||||
'id': input_id,
|
||||
'input_state': self.input_state[input_id],
|
||||
'feedback': {'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode, }}
|
||||
|
||||
@@ -134,6 +134,8 @@ class InputTypeBase(object):
|
||||
* 'id' -- the id of this input, typically
|
||||
"{problem-location}_{response-num}_{input-num}"
|
||||
* 'status' (answered, unanswered, unsubmitted)
|
||||
* 'input_state' -- dictionary containing any inputtype-specific state
|
||||
that has been preserved
|
||||
* 'feedback' (dictionary containing keys for hints, errors, or other
|
||||
feedback from previous attempt. Specifically 'message', 'hint',
|
||||
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
|
||||
@@ -160,6 +162,7 @@ class InputTypeBase(object):
|
||||
self.msg = feedback.get('message', '')
|
||||
self.hint = feedback.get('hint', '')
|
||||
self.hintmode = feedback.get('hintmode', None)
|
||||
self.input_state = state.get('input_state', {})
|
||||
|
||||
# put hint above msg if it should be displayed
|
||||
if self.hintmode == 'always':
|
||||
@@ -643,8 +646,11 @@ class MatlabInput(CodeInput):
|
||||
self.queue_len = 0
|
||||
self.queuename = 'matlab'
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of
|
||||
self.queue_msg = None
|
||||
# queue
|
||||
if self.status == 'incomplete':
|
||||
if 'queue_msg' in self.input_state:
|
||||
self.queue_msg = self.input_state['queue_msg']
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = self.submitted_msg
|
||||
@@ -652,13 +658,47 @@ class MatlabInput(CodeInput):
|
||||
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
''' Handle AJAX calls directed to this input'''
|
||||
if dispatch == 'plot':
|
||||
return self.plot_data(get)
|
||||
elif dispatch == 'xqueue_response':
|
||||
# render the response
|
||||
pass
|
||||
return self._plot_data(get)
|
||||
|
||||
def plot_data(self, get):
|
||||
def ungraded_response(self, queue_msg, queuekey):
|
||||
''' Handle any XQueue responses that have to be saved and rendered '''
|
||||
# check the queuekey against the saved queuekey
|
||||
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
|
||||
and self.input_state['queuekey'] == queuekey):
|
||||
msg = _parse_message(queue_msg)
|
||||
# save the queue message so that it can be rendered later
|
||||
self.input_state['queue_msg'] = msg
|
||||
self.input_state['queued'] = 'dequeued'
|
||||
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
extra_context = {'queue_len': self.queue_len}
|
||||
if self.queue_msg is not None:
|
||||
extra_context['queue_msg'] = self.queue_msg
|
||||
else:
|
||||
extra_context['queue_msg'] = ''
|
||||
return extra_context
|
||||
|
||||
def _parse_data(self, queue_msg):
|
||||
'''
|
||||
takes a queue_msg returned from the queue and parses it and returns
|
||||
whatever is stored in msg
|
||||
returns string msg
|
||||
'''
|
||||
try:
|
||||
result = json.loads(queue_msg)
|
||||
except (TypeError, ValueError):
|
||||
log.error("External message should be a JSON serialized dict."
|
||||
" Received queue_msg = %s" % queue_msg)
|
||||
raise
|
||||
# TODO: needs more error checking
|
||||
msg = result['msg']
|
||||
return msg
|
||||
|
||||
|
||||
def _plot_data(self, get):
|
||||
''' send data via xqueue to the mathworks backend'''
|
||||
# only send data if xqueue exists
|
||||
if self.system.xqueue is not None:
|
||||
@@ -668,7 +708,7 @@ class MatlabInput(CodeInput):
|
||||
# construct xqueue headers
|
||||
qinterface = self.system.xqueue['interface']
|
||||
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
|
||||
callback_url = self.system.xqueue['construct_callback']('input_ajax')
|
||||
callback_url = self.system.xqueue['construct_callback']('ungraded_response')
|
||||
anonymous_student_id = self.system.anonymous_student_id
|
||||
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
|
||||
anonymous_student_id +
|
||||
@@ -678,6 +718,10 @@ class MatlabInput(CodeInput):
|
||||
lms_key = queuekey,
|
||||
queue_name = self.queuename)
|
||||
|
||||
# save the input state
|
||||
self.input_state['queuekey'] = queuekey
|
||||
self.input_state['queuestate'] = 'queued'
|
||||
|
||||
|
||||
# construct xqueue body
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<div class="external-grader-message">
|
||||
${msg|n}
|
||||
</div>
|
||||
<div class="external-grader-message">
|
||||
${queue_msg|n}
|
||||
</div>
|
||||
|
||||
<div class="plot-button">
|
||||
<input type="button" name="plot-button" value="Plot" />
|
||||
|
||||
@@ -344,6 +344,35 @@ class MatlabTest(unittest.TestCase):
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': '',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
'queue_len': '3',
|
||||
}
|
||||
|
||||
self.assertEqual(context, expected)
|
||||
|
||||
|
||||
def test_rendering_with_state(self):
|
||||
state = {'value': 'print "good evening"',
|
||||
'status': 'incomplete',
|
||||
'input_state': {'queue_msg': 'message'},
|
||||
'feedback': {'message': '3'}, }
|
||||
elt = etree.fromstring(self.xml)
|
||||
|
||||
input_class = lookup_tag('matlabinput')
|
||||
the_input = self.input_class(test_system, elt, state)
|
||||
context = the_input._get_render_context()
|
||||
|
||||
expected = {'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
'queue_msg': 'message',
|
||||
'linenumbers': 'true',
|
||||
'hidden': '',
|
||||
'tabsize': int(self.tabsize),
|
||||
@@ -358,8 +387,9 @@ class MatlabTest(unittest.TestCase):
|
||||
|
||||
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
|
||||
|
||||
|
||||
self.assertTrue(response['success'])
|
||||
self.assertTrue(self.the_input.input_state['queuekey'] is not None)
|
||||
self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class CapaFields(object):
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={})
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
|
||||
display_name = String(help="Display name for this module", scope=Scope.settings)
|
||||
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
|
||||
'done': self.done,
|
||||
'correct_map': self.correct_map,
|
||||
'student_answers': self.student_answers,
|
||||
'input_state': self.input_state,
|
||||
'seed': self.seed,
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
|
||||
lcp_state = self.lcp.get_state()
|
||||
self.done = lcp_state['done']
|
||||
self.correct_map = lcp_state['correct_map']
|
||||
self.input_state = lcp_state['input_state']
|
||||
self.student_answers = lcp_state['student_answers']
|
||||
self.seed = lcp_state['seed']
|
||||
|
||||
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
|
||||
'problem_save': self.save_problem,
|
||||
'problem_show': self.get_answer,
|
||||
'score_update': self.update_score,
|
||||
'input_ajax': self.lcp.handle_input_ajax
|
||||
'input_ajax': self.lcp.handle_input_ajax,
|
||||
'ungraded_response': self.handle_ungraded_response
|
||||
}
|
||||
|
||||
if dispatch not in handlers:
|
||||
@@ -537,6 +541,17 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def handle_ungraded_response(self, get):
|
||||
'''
|
||||
Get the XQueue response
|
||||
'''
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
# pass along the xqueue message to the problem
|
||||
self.lcp.ungraded_response(score_msg, queuekey)
|
||||
|
||||
self.set_state_from_lcp()
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
For the "show answer" button.
|
||||
|
||||
Reference in New Issue
Block a user