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): ...@@ -111,6 +111,7 @@ class LoncapaProblem(object):
if self.system is None: if self.system is None:
raise Exception() raise Exception()
self.seed = seed self.seed = seed
self.input_state = None
if state: if state:
if 'seed' in state: if 'seed' in state:
...@@ -121,11 +122,16 @@ class LoncapaProblem(object): ...@@ -121,11 +122,16 @@ class LoncapaProblem(object):
self.correct_map.set_dict(state['correct_map']) self.correct_map.set_dict(state['correct_map'])
if 'done' in state: if 'done' in state:
self.done = state['done'] 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? # TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed: if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0] 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> # Convert startouttext and endouttext to proper <text></text>
problem_text = re.sub("startouttext\s*/", "text", problem_text) problem_text = re.sub("startouttext\s*/", "text", problem_text)
problem_text = re.sub("endouttext\s*/", "/text", problem_text) problem_text = re.sub("endouttext\s*/", "/text", problem_text)
...@@ -188,6 +194,7 @@ class LoncapaProblem(object): ...@@ -188,6 +194,7 @@ class LoncapaProblem(object):
return {'seed': self.seed, return {'seed': self.seed,
'student_answers': self.student_answers, 'student_answers': self.student_answers,
'correct_map': self.correct_map.get_dict(), 'correct_map': self.correct_map.get_dict(),
'input_state': self.input_state,
'done': self.done} 'done': self.done}
def get_max_score(self): def get_max_score(self):
...@@ -237,6 +244,19 @@ class LoncapaProblem(object): ...@@ -237,6 +244,19 @@ class LoncapaProblem(object):
self.correct_map.set_dict(cmap.get_dict()) self.correct_map.set_dict(cmap.get_dict())
return cmap 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): def is_queued(self):
''' '''
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
...@@ -527,11 +547,15 @@ class LoncapaProblem(object): ...@@ -527,11 +547,15 @@ class LoncapaProblem(object):
value = "" value = ""
if self.student_answers and problemid in self.student_answers: if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid] value = self.student_answers[problemid]
if input_id not in self.input_state:
self.input_state[input_id] = {}
# do the rendering # do the rendering
state = {'value': value, state = {'value': value,
'status': status, 'status': status,
'id': input_id, 'id': input_id,
'input_state': self.input_state[input_id],
'feedback': {'message': msg, 'feedback': {'message': msg,
'hint': hint, 'hint': hint,
'hintmode': hintmode, }} 'hintmode': hintmode, }}
......
...@@ -134,6 +134,8 @@ class InputTypeBase(object): ...@@ -134,6 +134,8 @@ class InputTypeBase(object):
* 'id' -- the id of this input, typically * 'id' -- the id of this input, typically
"{problem-location}_{response-num}_{input-num}" "{problem-location}_{response-num}_{input-num}"
* 'status' (answered, unanswered, unsubmitted) * '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' (dictionary containing keys for hints, errors, or other
feedback from previous attempt. Specifically 'message', 'hint', feedback from previous attempt. Specifically 'message', 'hint',
'hintmode'. If 'hintmode' is 'always', the hint is always displayed.) 'hintmode'. If 'hintmode' is 'always', the hint is always displayed.)
...@@ -160,6 +162,7 @@ class InputTypeBase(object): ...@@ -160,6 +162,7 @@ class InputTypeBase(object):
self.msg = feedback.get('message', '') self.msg = feedback.get('message', '')
self.hint = feedback.get('hint', '') self.hint = feedback.get('hint', '')
self.hintmode = feedback.get('hintmode', None) self.hintmode = feedback.get('hintmode', None)
self.input_state = state.get('input_state', {})
# put hint above msg if it should be displayed # put hint above msg if it should be displayed
if self.hintmode == 'always': if self.hintmode == 'always':
...@@ -643,8 +646,11 @@ class MatlabInput(CodeInput): ...@@ -643,8 +646,11 @@ class MatlabInput(CodeInput):
self.queue_len = 0 self.queue_len = 0
self.queuename = 'matlab' self.queuename = 'matlab'
# Flag indicating that the problem has been queued, 'msg' is length of # Flag indicating that the problem has been queued, 'msg' is length of
self.queue_msg = None
# queue # queue
if self.status == 'incomplete': if self.status == 'incomplete':
if 'queue_msg' in self.input_state:
self.queue_msg = self.input_state['queue_msg']
self.status = 'queued' self.status = 'queued'
self.queue_len = self.msg self.queue_len = self.msg
self.msg = self.submitted_msg self.msg = self.submitted_msg
...@@ -652,13 +658,47 @@ class MatlabInput(CodeInput): ...@@ -652,13 +658,47 @@ class MatlabInput(CodeInput):
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' Handle AJAX calls directed to this input'''
if dispatch == 'plot': if dispatch == 'plot':
return self.plot_data(get) return self._plot_data(get)
elif dispatch == 'xqueue_response':
# render the response def ungraded_response(self, queue_msg, queuekey):
pass ''' 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''' ''' send data via xqueue to the mathworks backend'''
# only send data if xqueue exists # only send data if xqueue exists
if self.system.xqueue is not None: if self.system.xqueue is not None:
...@@ -668,7 +708,7 @@ class MatlabInput(CodeInput): ...@@ -668,7 +708,7 @@ class MatlabInput(CodeInput):
# construct xqueue headers # construct xqueue headers
qinterface = self.system.xqueue['interface'] qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) 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 anonymous_student_id = self.system.anonymous_student_id
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
anonymous_student_id + anonymous_student_id +
...@@ -678,6 +718,10 @@ class MatlabInput(CodeInput): ...@@ -678,6 +718,10 @@ class MatlabInput(CodeInput):
lms_key = queuekey, lms_key = queuekey,
queue_name = self.queuename) queue_name = self.queuename)
# save the input state
self.input_state['queuekey'] = queuekey
self.input_state['queuestate'] = 'queued'
# construct xqueue body # construct xqueue body
student_info = {'anonymous_student_id': anonymous_student_id, student_info = {'anonymous_student_id': anonymous_student_id,
......
...@@ -29,6 +29,9 @@ ...@@ -29,6 +29,9 @@
<div class="external-grader-message"> <div class="external-grader-message">
${msg|n} ${msg|n}
</div> </div>
<div class="external-grader-message">
${queue_msg|n}
</div>
<div class="plot-button"> <div class="plot-button">
<input type="button" name="plot-button" value="Plot" /> <input type="button" name="plot-button" value="Plot" />
......
...@@ -344,6 +344,35 @@ class MatlabTest(unittest.TestCase): ...@@ -344,6 +344,35 @@ class MatlabTest(unittest.TestCase):
'mode': self.mode, 'mode': self.mode,
'rows': self.rows, 'rows': self.rows,
'cols': self.cols, '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', 'linenumbers': 'true',
'hidden': '', 'hidden': '',
'tabsize': int(self.tabsize), 'tabsize': int(self.tabsize),
...@@ -358,8 +387,9 @@ class MatlabTest(unittest.TestCase): ...@@ -358,8 +387,9 @@ class MatlabTest(unittest.TestCase):
test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY) test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
self.assertTrue(response['success']) 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): ...@@ -93,6 +93,7 @@ class CapaFields(object):
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings) rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) 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={}) 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) 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) 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) display_name = String(help="Display name for this module", scope=Scope.settings)
...@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule): ...@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
'done': self.done, 'done': self.done,
'correct_map': self.correct_map, 'correct_map': self.correct_map,
'student_answers': self.student_answers, 'student_answers': self.student_answers,
'input_state': self.input_state,
'seed': self.seed, 'seed': self.seed,
} }
...@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule): ...@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
lcp_state = self.lcp.get_state() lcp_state = self.lcp.get_state()
self.done = lcp_state['done'] self.done = lcp_state['done']
self.correct_map = lcp_state['correct_map'] self.correct_map = lcp_state['correct_map']
self.input_state = lcp_state['input_state']
self.student_answers = lcp_state['student_answers'] self.student_answers = lcp_state['student_answers']
self.seed = lcp_state['seed'] self.seed = lcp_state['seed']
...@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule): ...@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
'problem_save': self.save_problem, 'problem_save': self.save_problem,
'problem_show': self.get_answer, 'problem_show': self.get_answer,
'score_update': self.update_score, '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: if dispatch not in handlers:
...@@ -537,6 +541,17 @@ class CapaModule(CapaFields, XModule): ...@@ -537,6 +541,17 @@ class CapaModule(CapaFields, XModule):
return dict() # No AJAX return is needed 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): def get_answer(self, get):
''' '''
For the "show answer" button. 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