Commit af1af8c6 by Diana Huang

Address code review feedback:

- improve docstrings
- only pass in the state for a particular input and
not the whole dictionary
- refactor some common code
- minor syntax cleanup
parent 10c6e761
...@@ -91,8 +91,12 @@ class LoncapaProblem(object): ...@@ -91,8 +91,12 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem - problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces) - id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state - state (dict): containing the following keys:
- seed (int): random number generator seed (int) - 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
- 'correct_map' (CorrectMap) a map of each input to their 'correctness'
- 'done' - (bool) indicates whether or not this problem is considered done
- 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input
- system (ModuleSystem): ModuleSystem instance which provides OS, - system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context rendering, and user context
...@@ -104,27 +108,16 @@ class LoncapaProblem(object): ...@@ -104,27 +108,16 @@ class LoncapaProblem(object):
self.system = system self.system = system
if self.system is None: if self.system is None:
raise Exception() raise Exception()
self.seed = seed
self.input_state = None state = state if state else {}
self.seed = seed if seed else state.get('seed', struct.unpack('i', os.urandom(4))[0])
if state: self.student_answers = state.get('student_answers', {})
if 'seed' in state: if 'correct_map' in state:
self.seed = state['seed'] self.correct_map.set_dict(state['correct_map'])
if 'student_answers' in state: self.done = state.get('done', False)
self.student_answers = state['student_answers'] self.input_state = state.get('input_state', {})
if 'correct_map' in state:
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> # 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)
...@@ -240,13 +233,14 @@ class LoncapaProblem(object): ...@@ -240,13 +233,14 @@ class LoncapaProblem(object):
def ungraded_response(self, xqueue_msg, queuekey): def ungraded_response(self, xqueue_msg, queuekey):
''' '''
Handle any responses from the xqueue that are not related to grading Handle any responses from the xqueue that do not contain grades
Will try to pass the queue message to all inputtypes that can handle ungraded responses
Does not return any value Does not return any value
''' '''
# check against each inputtype # check against each inputtype
for the_input in self.inputs.values(): for the_input in self.inputs.values():
# if the input type has an xqueue_response function, pass in the values # if the input type has an ungraded function, pass in the values
if hasattr(the_input, 'ungraded_response'): if hasattr(the_input, 'ungraded_response'):
the_input.ungraded_response(xqueue_msg, queuekey) the_input.ungraded_response(xqueue_msg, queuekey)
...@@ -542,11 +536,14 @@ class LoncapaProblem(object): ...@@ -542,11 +536,14 @@ class LoncapaProblem(object):
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_state': self.input_state[input_id],
'feedback': {'message': msg, 'feedback': {'message': msg,
'hint': hint, 'hint': hint,
'hintmode': hintmode, }} 'hintmode': hintmode, }}
......
...@@ -161,7 +161,7 @@ class InputTypeBase(object): ...@@ -161,7 +161,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_dict = state.get('input_state', {}) 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':
...@@ -591,14 +591,14 @@ class CodeInput(InputTypeBase): ...@@ -591,14 +591,14 @@ class CodeInput(InputTypeBase):
Attribute('tabsize', 4, transform=int), Attribute('tabsize', 4, transform=int),
] ]
def setup(self): def setup_code_response_rendering(self):
""" """
Implement special logic: handle queueing state, and default input. Implement special logic: handle queueing state, and default input.
""" """
# if no student input yet, then use the default input given by the # if no student input yet, then use the default input given by the
# problem # problem
if not self.value: if not self.value and self.xml.text:
self.value = self.xml.text self.value = self.xml.text.strip()
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0 self.queue_len = 0
...@@ -609,6 +609,11 @@ class CodeInput(InputTypeBase): ...@@ -609,6 +609,11 @@ class CodeInput(InputTypeBase):
self.queue_len = self.msg self.queue_len = self.msg
self.msg = self.submitted_msg self.msg = self.submitted_msg
def setup(self):
''' setup this input type '''
self.setup_code_response_rendering()
def _extra_context(self): def _extra_context(self):
"""Defined queue_len, add it """ """Defined queue_len, add it """
return {'queue_len': self.queue_len, } return {'queue_len': self.queue_len, }
...@@ -623,8 +628,10 @@ class MatlabInput(CodeInput): ...@@ -623,8 +628,10 @@ class MatlabInput(CodeInput):
''' '''
InputType for handling Matlab code input InputType for handling Matlab code input
TODO: API_KEY will go away once we have a way to specify it per-course
Example: Example:
<matlabinput rows="10" cols="80" tabsize="4"> <matlabinput rows="10" cols="80" tabsize="4">
Initial Text
<plot_payload> <plot_payload>
%api_key=API_KEY %api_key=API_KEY
</plot_payload> </plot_payload>
...@@ -633,51 +640,56 @@ class MatlabInput(CodeInput): ...@@ -633,51 +640,56 @@ class MatlabInput(CodeInput):
template = "matlabinput.html" template = "matlabinput.html"
tags = ['matlabinput'] tags = ['matlabinput']
# pulled out for testing plot_submitted_msg = ("Submitted. As soon as a response is returned, "
submitted_msg = ("Submitted. As soon as your submission is" "this message will be replaced by that feedback.")
" graded, this message will be replaced with the grader's feedback.")
def setup(self): def setup(self):
''' '''
Handle matlab-specific parsing Handle matlab-specific parsing
''' '''
# if we don't have state for this input type yet, make one self.setup_code_response_rendering()
if self.id not in self.input_state_dict:
self.input_state_dict[self.id] = {}
self.input_state = self.input_state_dict[self.id]
xml = self.xml xml = self.xml
self.plot_payload = xml.findtext('./plot_payload') self.plot_payload = xml.findtext('./plot_payload')
# if no student input yet, then use the default input given by the
# problem
if not self.value:
self.value = self.xml.text
# Check if problem has been queued # Check if problem has been queued
self.queue_len = 0
self.queuename = 'matlab' self.queuename = 'matlab'
# Flag indicating that the problem has been queued, 'msg' is length of
self.queue_msg = '' self.queue_msg = ''
if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']: if 'queue_msg' in self.input_state and self.status in ['incomplete', 'unsubmitted']:
self.queue_msg = self.input_state['queue_msg'] self.queue_msg = self.input_state['queue_msg']
if 'queued' in self.input_state and self.input_state['queuestate'] is not None: if 'queued' in self.input_state and self.input_state['queuestate'] is not None:
self.status = 'queued' self.status = 'queued'
self.queue_len = 1 self.queue_len = 1
# queue self.msg = self.plot_submitted_msg
if self.status == 'incomplete':
self.status = 'queued'
self.queue_len = self.msg
self.msg = self.submitted_msg
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' Handle AJAX calls directed to this input''' '''
Handle AJAX calls directed to this input
Args:
- dispatch (str) - indicates how we want this ajax call to be handled
- get (dict) - dictionary of key-value pairs that contain useful data
Returns:
'''
if dispatch == 'plot': if dispatch == 'plot':
return self._plot_data(get) return self._plot_data(get)
return {}
def ungraded_response(self, queue_msg, queuekey): def ungraded_response(self, queue_msg, queuekey):
''' Handle any XQueue responses that have to be saved and rendered ''' '''
Handle the response from the XQueue
Stores the response in the input_state so it can be rendered later
Args:
- queue_msg (str) - message returned from the queue. The message to be rendered
- queuekey (str) - a key passed to the queue. Will be matched up to verify that this is the response we're waiting for
Returns:
nothing
'''
# check the queuekey against the saved queuekey # check the queuekey against the saved queuekey
if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued' if('queuestate' in self.input_state and self.input_state['queuestate'] == 'queued'
and self.input_state['queuekey'] == queuekey): and self.input_state['queuekey'] == queuekey):
...@@ -697,9 +709,11 @@ class MatlabInput(CodeInput): ...@@ -697,9 +709,11 @@ class MatlabInput(CodeInput):
def _parse_data(self, queue_msg): def _parse_data(self, queue_msg):
''' '''
takes a queue_msg returned from the queue and parses it and returns Parses the message out of the queue message
whatever is stored in msg Args:
returns string msg queue_msg (str) - a JSON encoded string
Returns:
returns the value for the the key 'msg' in queue_msg
''' '''
try: try:
result = json.loads(queue_msg) result = json.loads(queue_msg)
...@@ -712,42 +726,50 @@ class MatlabInput(CodeInput): ...@@ -712,42 +726,50 @@ class MatlabInput(CodeInput):
def _plot_data(self, get): def _plot_data(self, get):
''' send data via xqueue to the mathworks backend''' '''
AJAX handler for the plot button
Args:
get (dict) - should have key 'submission' which contains the student submission
Returns:
dict - 'success' - whether or not we successfully queued this submission
- 'message' - message to be rendered in case of error
'''
# only send data if xqueue exists # only send data if xqueue exists
if self.system.xqueue is not None: if self.system.xqueue is None:
# pull relevant info out of get return {'success': False, 'message': 'Cannot connect to the queue'}
response = get['submission']
# pull relevant info out of get
# construct xqueue headers response = get['submission']
qinterface = self.system.xqueue['interface']
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) # construct xqueue headers
callback_url = self.system.xqueue['construct_callback']('ungraded_response') qinterface = self.system.xqueue['interface']
anonymous_student_id = self.system.anonymous_student_id qtime = datetime.strftime(datetime.utcnow(), xqueue_interface.dateformat)
queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime + callback_url = self.system.xqueue['construct_callback']('ungraded_response')
anonymous_student_id + anonymous_student_id = self.system.anonymous_student_id
self.id) queuekey = xqueue_interface.make_hashkey(str(self.system.seed) + qtime +
xheader = xqueue_interface.make_xheader( anonymous_student_id +
lms_callback_url = callback_url, self.id)
lms_key = queuekey, xheader = xqueue_interface.make_xheader(
queue_name = self.queuename) lms_callback_url = callback_url,
lms_key = queuekey,
# save the input state queue_name = self.queuename)
self.input_state['queuekey'] = queuekey
self.input_state['queuestate'] = 'queued' # 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,
'submission_time': qtime} # construct xqueue body
contents = {'grader_payload': self.plot_payload, student_info = {'anonymous_student_id': anonymous_student_id,
'student_info': json.dumps(student_info), 'submission_time': qtime}
'student_response': response} contents = {'grader_payload': self.plot_payload,
'student_info': json.dumps(student_info),
(error, msg) = qinterface.send_to_queue(header=xheader, 'student_response': response}
body = json.dumps(contents))
(error, msg) = qinterface.send_to_queue(header=xheader,
return {'success': error == 0, 'message': msg} body = json.dumps(contents))
return {'success': False, 'message': 'Cannot connect to the queue'}
return {'success': error == 0, 'message': msg}
registry.register(MatlabInput) registry.register(MatlabInput)
......
...@@ -1147,10 +1147,10 @@ def sympy_check2(): ...@@ -1147,10 +1147,10 @@ def sympy_check2():
correct = [] correct = []
messages = [] messages = []
for input_dict in input_list: for input_dict in input_list:
correct.append('correct' if input_dict[ correct.append('correct'
'ok'] else 'incorrect') if input_dict['ok'] else 'incorrect')
msg = self.clean_message_html(input_dict[ msg = (self.clean_message_html(input_dict['msg'])
'msg']) if 'msg' in input_dict else None if 'msg' in input_dict else None)
messages.append(msg) messages.append(msg)
# Otherwise, we do not recognize the dictionary # Otherwise, we do not recognize the dictionary
...@@ -1164,8 +1164,8 @@ def sympy_check2(): ...@@ -1164,8 +1164,8 @@ def sympy_check2():
# indicating whether all inputs should be marked # indicating whether all inputs should be marked
# correct or incorrect # correct or incorrect
else: else:
correct = ['correct'] * len( n = len(idset)
idset) if ret else ['incorrect'] * len(idset) correct = ['correct'] * n if ret else ['incorrect'] * n
# build map giving "correct"ness of the answer(s) # build map giving "correct"ness of the answer(s)
correct_map = CorrectMap() correct_map = CorrectMap()
...@@ -1174,8 +1174,8 @@ def sympy_check2(): ...@@ -1174,8 +1174,8 @@ def sympy_check2():
correct_map.set_overall_message(overall_message) correct_map.set_overall_message(overall_message)
for k in range(len(idset)): for k in range(len(idset)):
npoints = self.maxpoints[idset[ npoints = (self.maxpoints[idset[k]]
k]] if correct[k] == 'correct' else 0 if correct[k] == 'correct' else 0)
correct_map.set(idset[k], correct[k], msg=messages[k], correct_map.set(idset[k], correct[k], msg=messages[k],
npoints=npoints) npoints=npoints)
return correct_map return correct_map
......
...@@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase): ...@@ -357,7 +357,7 @@ class MatlabTest(unittest.TestCase):
def test_rendering_with_state(self): def test_rendering_with_state(self):
state = {'value': 'print "good evening"', state = {'value': 'print "good evening"',
'status': 'incomplete', 'status': 'incomplete',
'input_state': {'prob_1_2': {'queue_msg': 'message'}}, 'input_state': {'queue_msg': 'message'},
'feedback': {'message': '3'}, } 'feedback': {'message': '3'}, }
elt = etree.fromstring(self.xml) elt = etree.fromstring(self.xml)
......
...@@ -543,8 +543,16 @@ class CapaModule(CapaFields, XModule): ...@@ -543,8 +543,16 @@ class CapaModule(CapaFields, XModule):
def handle_ungraded_response(self, get): def handle_ungraded_response(self, get):
''' '''
Delivers a response to the capa problem where the expectation where this response does Delivers a response from the XQueue to the capa problem
not have relevant grading information
The score of the problem will not be updated
Args:
- get (dict) must contain keys:
queuekey - a key specific to this response
xqueue_body - the body of the response
Returns:
empty dictionary
No ajax return is needed, so an empty dict is returned No ajax return is needed, so an empty dict is returned
''' '''
...@@ -557,8 +565,12 @@ class CapaModule(CapaFields, XModule): ...@@ -557,8 +565,12 @@ class CapaModule(CapaFields, XModule):
def handle_input_ajax(self, get): def handle_input_ajax(self, get):
''' '''
Passes information down to the capa problem so that it can handle its own ajax calls Handle ajax calls meant for a particular input in the problem
Returns the response from the capa problem
Args:
- get (dict) - data that should be passed to the input
Returns:
- dict containing the response from the input
''' '''
response = self.lcp.handle_input_ajax(get) response = self.lcp.handle_input_ajax(get)
# save any state changes that may occur # save any state changes that may occur
......
...@@ -183,7 +183,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -183,7 +183,7 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location self.test_system.location = self.location
self.mock_xqueue = MagicMock() self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") self.mock_xqueue.send_to_queue.return_value = (None, "Message")
def constructed_callback(dispatch = "score_update"): def constructed_callback(dispatch="score_update"):
return dispatch return dispatch
self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue', self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue',
......
...@@ -182,7 +182,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours ...@@ -182,7 +182,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http') proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
) )
def make_xqueue_callback(dispatch = 'score_update'): def make_xqueue_callback(dispatch='score_update'):
# Fully qualified callback URL for external queueing system # Fully qualified callback URL for external queueing system
xqueue_callback_url = '{proto}://{host}'.format( xqueue_callback_url = '{proto}://{host}'.format(
host=request.get_host(), host=request.get_host(),
......
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