diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 42753fc90b..fbf911e500 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -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 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, }} diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 08c395692f..303619c820 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -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, diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index ba516be249..07433f0a3a 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -29,6 +29,9 @@
${msg|n}
+
+ ${queue_msg|n} +
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 01801ac822..97e27d5ffc 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -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') diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index e66b1d3495..1bdd62f5b7 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -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.