Commit 521c469a by Diana Huang

Add the ability for input types to have their own state

and add in a handler for ungraded responses via xqueue
parent 45d8086e
......@@ -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 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 plot_data(self, get):
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.
......
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