Commit b1d91fad by Sarina Canelake

Merge pull request #1663 from edx/sarina/persist-student-answers-on-error

Sarina/persist student answers on error
parents dffbb8a3 9508b3f9
......@@ -655,6 +655,7 @@ class ChoiceResponse(LoncapaResponse):
response_tag = 'choiceresponse'
max_inputfields = 1
allowed_inputfields = ['checkboxgroup', 'radiogroup']
correct_choices = None
def setup_response(self):
......@@ -706,6 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
response_tag = 'multiplechoiceresponse'
max_inputfields = 1
allowed_inputfields = ['choicegroup']
correct_choices = None
def setup_response(self):
# call secondary setup for MultipleChoice questions, to set name
......@@ -791,6 +793,7 @@ class OptionResponse(LoncapaResponse):
response_tag = 'optionresponse'
hint_tag = 'optionhint'
allowed_inputfields = ['optioninput']
answer_fields = None
def setup_response(self):
self.answer_fields = self.inputfields
......@@ -949,6 +952,7 @@ class StringResponse(LoncapaResponse):
allowed_inputfields = ['textline']
required_attributes = ['answer']
max_inputfields = 1
correct_answer = None
def setup_response(self):
self.correct_answer = contextualize_text(
......@@ -974,7 +978,7 @@ class StringResponse(LoncapaResponse):
hxml.get('answer'), self.context).strip()
if self.check_string(correct_answer, given):
hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show)
log.debug('hints_to_show = %s', hints_to_show)
return hints_to_show
def get_answers(self):
......@@ -996,6 +1000,8 @@ class CustomResponse(LoncapaResponse):
'drag_and_drop_input', 'editamoleculeinput',
'designprotein2dinput', 'editageneinput',
'annotationinput', 'jsinput', 'formulaequationinput']
code = None
expect = None
def setup_response(self):
xml = self.xml
......@@ -1004,7 +1010,7 @@ class CustomResponse(LoncapaResponse):
# that
self.expect = xml.get('expect') or xml.get('answer')
log.debug('answer_ids=%s' % self.answer_ids)
log.debug('answer_ids=%s', self.answer_ids)
# the <answer>...</answer> stanza should be local to the current <customresponse>.
# So try looking there first.
......@@ -1020,7 +1026,7 @@ class CustomResponse(LoncapaResponse):
# <script>...</script> stanza instead
cfn = xml.get('cfn')
if cfn:
log.debug("cfn = %s" % cfn)
log.debug("cfn = %s", cfn)
# This is a bit twisty. We used to grab the cfn function from
# the context, but now that we sandbox Python execution, we
......@@ -1055,7 +1061,7 @@ class CustomResponse(LoncapaResponse):
if not self.code:
if answer is None:
log.error("[courseware.capa.responsetypes.customresponse] missing"
" code checking script! id=%s" % self.id)
" code checking script! id=%s", self.id)
self.code = ''
else:
answer_src = answer.get('src')
......@@ -1071,7 +1077,7 @@ class CustomResponse(LoncapaResponse):
of each key removed (the string before the first "_").
'''
log.debug('%s: student_answers=%s' % (unicode(self), student_answers))
log.debug('%s: student_answers=%s', unicode(self), student_answers)
# ordered list of answer id's
idset = sorted(self.answer_ids)
......@@ -1182,10 +1188,10 @@ class CustomResponse(LoncapaResponse):
answer_given = submission[0] if (len(idset) == 1) else submission
kwnames = self.xml.get("cfn_extra_args", "").split()
kwargs = {n: self.context.get(n) for n in kwnames}
log.debug(" submission = %s" % submission)
log.debug(" submission = %s", submission)
try:
ret = fn(self.expect, answer_given, **kwargs)
except Exception as err:
except Exception as err: # pylint: disable=broad-except
self._handle_exec_exception(err)
log.debug(
"[courseware.capa.responsetypes.customresponse.get_score] ret = %s",
......@@ -1340,22 +1346,21 @@ class SymbolicResponse(CustomResponse):
debug=self.context.get('debug'),
)
except Exception as err:
log.error("oops in symbolicresponse (cfn) error %s" % err)
log.error("oops in symbolicresponse (cfn) error %s", err)
log.error(traceback.format_exc())
raise Exception("oops in symbolicresponse (cfn) error %s" % err)
raise Exception("oops in symbolicresponse (cfn) error %s", err)
self.context['messages'][0] = self.clean_message_html(ret['msg'])
self.context['correct'] = ['correct' if ret['ok'] else 'incorrect'] * len(idset)
#-----------------------------------------------------------------------------
"""
valid: Flag indicating valid score_msg format (Boolean)
correct: Correctness of submission (Boolean)
score: Points to be assigned (numeric, can be float)
msg: Message from grader to display to student (string)
"""
ScoreMessage = namedtuple('ScoreMessage',
['valid', 'correct', 'points', 'msg'])
## ScoreMessage named tuple ##
## valid: Flag indicating valid score_msg format (Boolean)
## correct: Correctness of submission (Boolean)
## score: Points to be assigned (numeric, can be float)
## msg: Message from grader to display to student (string)
ScoreMessage = namedtuple('ScoreMessage', ['valid', 'correct', 'points', 'msg']) # pylint: disable=invalid-name
class CodeResponse(LoncapaResponse):
......@@ -1377,6 +1382,11 @@ class CodeResponse(LoncapaResponse):
response_tag = 'coderesponse'
allowed_inputfields = ['textbox', 'filesubmission', 'matlabinput']
max_inputfields = 1
payload = None
initial_display = None
url = None
answer = None
queue_name = None
def setup_response(self):
'''
......@@ -1427,8 +1437,8 @@ class CodeResponse(LoncapaResponse):
except Exception as err:
log.error(
'Error in CodeResponse %s: cannot get student answer for %s;'
' student_answers=%s' %
(err, self.answer_id, convert_files_to_filenames(student_answers))
' student_answers=%s',
err, self.answer_id, convert_files_to_filenames(student_answers)
)
raise Exception(err)
......@@ -1511,9 +1521,8 @@ class CodeResponse(LoncapaResponse):
return cmap
def update_score(self, score_msg, oldcmap, queuekey):
(valid_score_msg, correct, points,
msg) = self._parse_score_msg(score_msg)
"""Updates the user's score based on the returned message from the grader."""
(valid_score_msg, correct, points, msg) = self._parse_score_msg(score_msg)
if not valid_score_msg:
oldcmap.set(self.answer_id,
msg='Invalid grader reply. Please contact the course staff.')
......@@ -1536,8 +1545,11 @@ class CodeResponse(LoncapaResponse):
self.answer_id, npoints=points, correctness=correctness,
msg=msg.replace('&nbsp;', '&#160;'), queuestate=None)
else:
log.debug('CodeResponse: queuekey %s does not match for answer_id=%s.' %
(queuekey, self.answer_id))
log.debug(
'CodeResponse: queuekey %s does not match for answer_id=%s.',
queuekey,
self.answer_id
)
return oldcmap
......@@ -1546,6 +1558,10 @@ class CodeResponse(LoncapaResponse):
return {self.answer_id: anshtml}
def get_initial_display(self):
"""
The course author can specify an initial display
to be displayed the code response box.
"""
return {self.answer_id: self.initial_display}
def _parse_score_msg(self, score_msg):
......@@ -1566,11 +1582,11 @@ class CodeResponse(LoncapaResponse):
score_result = json.loads(score_msg)
except (TypeError, ValueError):
log.error("External grader message should be a JSON-serialized dict."
" Received score_msg = %s" % score_msg)
" Received score_msg = %s", score_msg)
return fail
if not isinstance(score_result, dict):
log.error("External grader message should be a JSON-serialized dict."
" Received score_result = %s" % score_result)
" Received score_result = %s", score_result)
return fail
for tag in ['correct', 'score', 'msg']:
if tag not in score_result:
......@@ -1585,9 +1601,9 @@ class CodeResponse(LoncapaResponse):
msg = score_result['msg']
try:
etree.fromstring(msg)
except etree.XMLSyntaxError as err:
except etree.XMLSyntaxError as _err:
log.error("Unable to parse external grader message as valid"
" XML: score_msg['msg']=%s" % msg)
" XML: score_msg['msg']=%s", msg)
return fail
return (True, score_result['correct'], score_result['score'], msg)
......@@ -1805,7 +1821,10 @@ class FormulaResponse(LoncapaResponse):
def get_score(self, student_answers):
given = student_answers[self.answer_id]
correctness = self.check_formula(
self.correct_answer, given, self.samples)
self.correct_answer,
given,
self.samples
)
return CorrectMap(self.answer_id, correctness)
def tupleize_answers(self, answer, var_dict_list):
......
"""Implements basics of Capa, including class CapaModule."""
import cgi
import datetime
import hashlib
......@@ -40,11 +41,11 @@ def randomization_bin(seed, problem_id):
interesting. To avoid having sets of students that always get the same problems,
we'll combine the system's per-student seed with the problem id in picking the bin.
"""
h = hashlib.sha1()
h.update(str(seed))
h.update(str(problem_id))
r_hash = hashlib.sha1()
r_hash.update(str(seed))
r_hash.update(str(problem_id))
# get the first few digits of the hash, convert to an int, then mod.
return int(h.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
return int(r_hash.hexdigest()[:7], 16) % NUM_RANDOMIZATION_BINS
class Randomization(String):
......@@ -220,7 +221,7 @@ class CapaModule(CapaFields, XModule):
if self.seed is None:
self.seed = self.lcp.seed
except Exception as err:
except Exception as err: # pylint: disable=broad-except
msg = u'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
# TODO (vshnayder): do modules need error handlers too?
......@@ -318,9 +319,9 @@ class CapaModule(CapaFields, XModule):
"""
For now, just return score / max_score
"""
d = self.get_score()
score = d['score']
total = d['total']
score_dict = self.get_score()
score = score_dict['score']
total = score_dict['total']
if total > 0:
if self.weight is not None:
......@@ -525,7 +526,7 @@ class CapaModule(CapaFields, XModule):
# If we cannot construct the problem HTML,
# then generate an error message instead.
except Exception as err:
except Exception as err: # pylint: disable=broad-except
html = self.handle_problem_html_error(err)
# The convention is to pass the name of the check button
......@@ -610,11 +611,11 @@ class CapaModule(CapaFields, XModule):
result = handlers[dispatch](data)
except NotFoundError as err:
_, _, traceback_obj = sys.exc_info()
_, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name
raise ProcessingError, (not_found_error_message, err), traceback_obj
except Exception as err:
_, _, traceback_obj = sys.exc_info()
_, _, traceback_obj = sys.exc_info() # pylint: disable=redefined-outer-name
raise ProcessingError, (generic_error_message, err), traceback_obj
after = self.get_progress()
......@@ -668,8 +669,8 @@ class CapaModule(CapaFields, XModule):
"""
True iff full points
"""
d = self.get_score()
return d['score'] == d['total']
score_dict = self.get_score()
return score_dict['score'] == score_dict['total']
def answer_available(self):
"""
......@@ -757,7 +758,7 @@ class CapaModule(CapaFields, XModule):
self.set_state_from_lcp()
return response
def get_answer(self, data):
def get_answer(self, _data):
"""
For the "show answer" button.
......@@ -797,7 +798,6 @@ class CapaModule(CapaFields, XModule):
"""
return {'html': self.get_problem_html(encapsulate=False)}
@staticmethod
def make_dict_of_responses(data):
"""
......@@ -840,7 +840,7 @@ class CapaModule(CapaFields, XModule):
# We only want to consider each key a single time, so we use set(data.keys())
for key in set(data.keys()):
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
_, _, name = key.partition('_') # pylint: disable=redefined-outer-name
# If key has no underscores, then partition
# will return (key, '', '')
......@@ -941,6 +941,9 @@ class CapaModule(CapaFields, XModule):
log.warning("StudentInputError in capa_module:problem_check",
exc_info=True)
# Save the user's state before failing
self.set_state_from_lcp()
# If the user is a staff member, include
# the full exception, including traceback,
# in the response
......@@ -955,6 +958,9 @@ class CapaModule(CapaFields, XModule):
return {'success': msg}
except Exception as err:
# Save the user's state before failing
self.set_state_from_lcp()
if self.system.DEBUG:
msg = u"Error checking problem: {}".format(err.message)
msg += u'\nTraceback:\n{}'.format(traceback.format_exc())
......
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