Merge pull request #2219 from edx/sarina/i19-capa-new
i19 base capa strings
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
# Each Response may have one or more Input entry fields.
|
||||
# The capa problem may include a solution.
|
||||
#
|
||||
'''
|
||||
"""
|
||||
Main module which shows problems (of "capa" type).
|
||||
|
||||
This is used by capa_module.
|
||||
'''
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
@@ -108,9 +108,9 @@ class LoncapaSystem(object):
|
||||
|
||||
|
||||
class LoncapaProblem(object):
|
||||
'''
|
||||
"""
|
||||
Main class for capa Problems.
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, problem_text, id, capa_system, state=None, seed=None):
|
||||
"""
|
||||
@@ -181,9 +181,9 @@ class LoncapaProblem(object):
|
||||
self.extracted_tree = self._extract_html(self.tree)
|
||||
|
||||
def do_reset(self):
|
||||
'''
|
||||
"""
|
||||
Reset internal state to unfinished, with no answers
|
||||
'''
|
||||
"""
|
||||
self.student_answers = dict()
|
||||
self.correct_map = CorrectMap()
|
||||
self.done = False
|
||||
@@ -203,11 +203,11 @@ class LoncapaProblem(object):
|
||||
return u"LoncapaProblem ({0})".format(self.problem_id)
|
||||
|
||||
def get_state(self):
|
||||
'''
|
||||
"""
|
||||
Stored per-user session data neeeded to:
|
||||
1) Recreate the problem
|
||||
2) Populate any student answers.
|
||||
'''
|
||||
"""
|
||||
|
||||
return {'seed': self.seed,
|
||||
'student_answers': self.student_answers,
|
||||
@@ -216,9 +216,9 @@ class LoncapaProblem(object):
|
||||
'done': self.done}
|
||||
|
||||
def get_max_score(self):
|
||||
'''
|
||||
"""
|
||||
Return the maximum score for this problem.
|
||||
'''
|
||||
"""
|
||||
maxscore = 0
|
||||
for responder in self.responders.values():
|
||||
maxscore += responder.get_max_score()
|
||||
@@ -235,7 +235,7 @@ class LoncapaProblem(object):
|
||||
try:
|
||||
correct += self.correct_map.get_npoints(key)
|
||||
except Exception:
|
||||
log.error('key=%s, correct_map = %s' % (key, self.correct_map))
|
||||
log.error('key=%s, correct_map = %s', key, self.correct_map)
|
||||
raise
|
||||
|
||||
if (not self.student_answers) or len(self.student_answers) == 0:
|
||||
@@ -246,12 +246,12 @@ class LoncapaProblem(object):
|
||||
'total': self.get_max_score()}
|
||||
|
||||
def update_score(self, score_msg, queuekey):
|
||||
'''
|
||||
"""
|
||||
Deliver grading response (e.g. from async code checking) to
|
||||
the specific ResponseType that requested grading
|
||||
|
||||
Returns an updated CorrectMap
|
||||
'''
|
||||
"""
|
||||
cmap = CorrectMap()
|
||||
cmap.update(self.correct_map)
|
||||
for responder in self.responders.values():
|
||||
@@ -263,12 +263,12 @@ class LoncapaProblem(object):
|
||||
return cmap
|
||||
|
||||
def ungraded_response(self, xqueue_msg, queuekey):
|
||||
'''
|
||||
"""
|
||||
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
|
||||
'''
|
||||
"""
|
||||
# check against each inputtype
|
||||
for the_input in self.inputs.values():
|
||||
# if the input type has an ungraded function, pass in the values
|
||||
@@ -276,17 +276,17 @@ class LoncapaProblem(object):
|
||||
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
|
||||
(e.g. for grading.)
|
||||
'''
|
||||
"""
|
||||
return any(self.correct_map.is_queued(answer_id) for answer_id in self.correct_map)
|
||||
|
||||
def get_recentmost_queuetime(self):
|
||||
'''
|
||||
"""
|
||||
Returns a DateTime object that represents the timestamp of the most recent
|
||||
queueing request, or None if not queued
|
||||
'''
|
||||
"""
|
||||
if not self.is_queued():
|
||||
return None
|
||||
|
||||
@@ -304,7 +304,7 @@ class LoncapaProblem(object):
|
||||
return max(queuetimes)
|
||||
|
||||
def grade_answers(self, answers):
|
||||
'''
|
||||
"""
|
||||
Grade student responses. Called by capa_module.check_problem.
|
||||
|
||||
`answers` is a dict of all the entries from request.POST, but with the first part
|
||||
@@ -313,7 +313,7 @@ class LoncapaProblem(object):
|
||||
Thus, for example, input_ID123 -> ID123, and input_fromjs_ID123 -> fromjs_ID123
|
||||
|
||||
Calls the Response for each question in this problem, to do the actual grading.
|
||||
'''
|
||||
"""
|
||||
|
||||
# if answers include File objects, convert them to filenames.
|
||||
self.student_answers = convert_files_to_filenames(answers)
|
||||
@@ -363,7 +363,6 @@ class LoncapaProblem(object):
|
||||
|
||||
# start new with empty CorrectMap
|
||||
newcmap = CorrectMap()
|
||||
|
||||
# Call each responsetype instance to do actual grading
|
||||
for responder in self.responders.values():
|
||||
# File objects are passed only if responsetype explicitly allows
|
||||
@@ -372,7 +371,8 @@ class LoncapaProblem(object):
|
||||
# an earlier submission, so for now skip these entirely.
|
||||
# TODO: figure out where to get file submissions when rescoring.
|
||||
if 'filesubmission' in responder.allowed_inputfields and student_answers is None:
|
||||
raise Exception("Cannot rescore problems with possible file submissions")
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
raise Exception(_("Cannot rescore problems with possible file submissions"))
|
||||
|
||||
# use 'student_answers' only if it is provided, and if it might contain a file
|
||||
# submission that would not exist in the persisted "student_answers".
|
||||
@@ -404,7 +404,7 @@ class LoncapaProblem(object):
|
||||
if answer:
|
||||
answer_map[entry.get('id')] = contextualize_text(answer, self.context)
|
||||
|
||||
log.debug('answer_map = %s' % answer_map)
|
||||
log.debug('answer_map = %s', answer_map)
|
||||
return answer_map
|
||||
|
||||
def get_answer_ids(self):
|
||||
@@ -420,18 +420,18 @@ class LoncapaProblem(object):
|
||||
return answer_ids
|
||||
|
||||
def get_html(self):
|
||||
'''
|
||||
"""
|
||||
Main method called externally to get the HTML to be rendered for this capa Problem.
|
||||
'''
|
||||
"""
|
||||
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
|
||||
return html
|
||||
|
||||
def handle_input_ajax(self, data):
|
||||
'''
|
||||
"""
|
||||
InputTypes can support specialized AJAX calls. Find the correct input and pass along the correct data
|
||||
|
||||
Also, parse out the dispatch from the get so that it can be passed onto the input type nicely
|
||||
'''
|
||||
"""
|
||||
|
||||
# pull out the id
|
||||
input_id = data['input_id']
|
||||
@@ -439,16 +439,16 @@ class LoncapaProblem(object):
|
||||
dispatch = data['dispatch']
|
||||
return self.inputs[input_id].handle_ajax(dispatch, data)
|
||||
else:
|
||||
log.warning("Could not find matching input for id: %s" % input_id)
|
||||
log.warning("Could not find matching input for id: %s", input_id)
|
||||
return {}
|
||||
|
||||
# ======= Private Methods Below ========
|
||||
|
||||
def _process_includes(self):
|
||||
'''
|
||||
"""
|
||||
Handle any <include file="foo"> tags by reading in the specified file and inserting it
|
||||
into our XML tree. Fail gracefully if debugging.
|
||||
'''
|
||||
"""
|
||||
includes = self.tree.findall('.//include')
|
||||
for inc in includes:
|
||||
filename = inc.get('file')
|
||||
@@ -458,14 +458,12 @@ class LoncapaProblem(object):
|
||||
ifp = self.capa_system.filestore.open(filename)
|
||||
except Exception as err:
|
||||
log.warning(
|
||||
'Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)
|
||||
)
|
||||
'Error %s in problem xml include: %s',
|
||||
err,
|
||||
etree.tostring(inc, pretty_print=True)
|
||||
)
|
||||
log.warning(
|
||||
'Cannot find file %s in %s' % (
|
||||
filename, self.capa_system.filestore
|
||||
)
|
||||
'Cannot find file %s in %s', filename, self.capa_system.filestore
|
||||
)
|
||||
# if debugging, don't fail - just log error
|
||||
# TODO (vshnayder): need real error handling, display to users
|
||||
@@ -478,11 +476,11 @@ class LoncapaProblem(object):
|
||||
incxml = etree.XML(ifp.read())
|
||||
except Exception as err:
|
||||
log.warning(
|
||||
'Error %s in problem xml include: %s' % (
|
||||
err, etree.tostring(inc, pretty_print=True)
|
||||
)
|
||||
'Error %s in problem xml include: %s',
|
||||
err,
|
||||
etree.tostring(inc, pretty_print=True)
|
||||
)
|
||||
log.warning('Cannot parse XML in %s' % (filename))
|
||||
log.warning('Cannot parse XML in %s', (filename))
|
||||
# if debugging, don't fail - just log error
|
||||
# TODO (vshnayder): same as above
|
||||
if not self.capa_system.DEBUG:
|
||||
@@ -522,7 +520,7 @@ class LoncapaProblem(object):
|
||||
# Check that we are within the filestore tree.
|
||||
reldir = os.path.relpath(dir, self.capa_system.filestore.root_path)
|
||||
if ".." in reldir:
|
||||
log.warning("Ignoring Python directory outside of course: %r" % dir)
|
||||
log.warning("Ignoring Python directory outside of course: %r", dir)
|
||||
continue
|
||||
|
||||
abs_dir = os.path.normpath(dir)
|
||||
@@ -531,13 +529,13 @@ class LoncapaProblem(object):
|
||||
return path
|
||||
|
||||
def _extract_context(self, tree):
|
||||
'''
|
||||
"""
|
||||
Extract content of <script>...</script> from the problem.xml file, and exec it in the
|
||||
context of this problem. Provides ability to randomize problems, and also set
|
||||
variables for problem answer checking.
|
||||
|
||||
Problem XML goes to Python execution context. Runs everything in script tags.
|
||||
'''
|
||||
"""
|
||||
context = {}
|
||||
context['seed'] = self.seed
|
||||
all_code = ''
|
||||
@@ -584,7 +582,7 @@ class LoncapaProblem(object):
|
||||
return context
|
||||
|
||||
def _extract_html(self, problemtree): # private
|
||||
'''
|
||||
"""
|
||||
Main (private) function which converts Problem XML tree to HTML.
|
||||
Calls itself recursively.
|
||||
|
||||
@@ -592,7 +590,7 @@ class LoncapaProblem(object):
|
||||
Calls render_html of Response instances to render responses into XHTML.
|
||||
|
||||
Used by get_html.
|
||||
'''
|
||||
"""
|
||||
if not isinstance(problemtree.tag, basestring):
|
||||
# Comment and ProcessingInstruction nodes are not Elements,
|
||||
# and we're ok leaving those behind.
|
||||
@@ -632,13 +630,17 @@ class LoncapaProblem(object):
|
||||
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, }}
|
||||
state = {
|
||||
'value': value,
|
||||
'status': status,
|
||||
'id': input_id,
|
||||
'input_state': self.input_state[input_id],
|
||||
'feedback': {
|
||||
'message': msg,
|
||||
'hint': hint,
|
||||
'hintmode': hintmode,
|
||||
}
|
||||
}
|
||||
|
||||
input_type_cls = inputtypes.registry.get_class_for_tag(problemtree.tag)
|
||||
# save the input type so that we can make ajax calls on it if we need to
|
||||
@@ -678,7 +680,7 @@ class LoncapaProblem(object):
|
||||
return tree
|
||||
|
||||
def _preprocess_problem(self, tree): # private
|
||||
'''
|
||||
"""
|
||||
Assign IDs to all the responses
|
||||
Assign sub-IDs to all entries (textline, schematic, etc.)
|
||||
Annoted correctness and value
|
||||
@@ -687,7 +689,7 @@ class LoncapaProblem(object):
|
||||
Also create capa Response instances for each responsetype and save as self.responders
|
||||
|
||||
Obtain all responder answers and save as self.responder_answers dict (key = response)
|
||||
'''
|
||||
"""
|
||||
response_id = 1
|
||||
self.responders = {}
|
||||
for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())):
|
||||
|
||||
@@ -63,13 +63,13 @@ class CorrectMap(object):
|
||||
return repr(self.cmap)
|
||||
|
||||
def get_dict(self):
|
||||
'''
|
||||
"""
|
||||
return dict version of self
|
||||
'''
|
||||
"""
|
||||
return self.cmap
|
||||
|
||||
def set_dict(self, correct_map):
|
||||
'''
|
||||
"""
|
||||
Set internal dict of CorrectMap to provided correct_map dict
|
||||
|
||||
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
|
||||
@@ -85,7 +85,7 @@ class CorrectMap(object):
|
||||
Special migration case:
|
||||
If correct_map is a one-level dict, then convert it to the new dict of dicts format.
|
||||
|
||||
'''
|
||||
"""
|
||||
# empty current dict
|
||||
self.__init__()
|
||||
|
||||
@@ -149,17 +149,17 @@ class CorrectMap(object):
|
||||
return self.get_property(answer_id, 'hintmode', None)
|
||||
|
||||
def set_hint_and_mode(self, answer_id, hint, hintmode):
|
||||
'''
|
||||
"""
|
||||
- hint : (string) HTML text for hint
|
||||
- hintmode : (string) mode for hint display ('always' or 'on_request')
|
||||
'''
|
||||
"""
|
||||
self.set_property(answer_id, 'hint', hint)
|
||||
self.set_property(answer_id, 'hintmode', hintmode)
|
||||
|
||||
def update(self, other_cmap):
|
||||
'''
|
||||
"""
|
||||
Update this CorrectMap with the contents of another CorrectMap
|
||||
'''
|
||||
"""
|
||||
if not isinstance(other_cmap, CorrectMap):
|
||||
raise Exception('CorrectMap.update called with invalid argument %s' % other_cmap)
|
||||
self.cmap.update(other_cmap.get_dict())
|
||||
|
||||
@@ -26,7 +26,7 @@ class MathRenderer(object):
|
||||
tags = ['math']
|
||||
|
||||
def __init__(self, system, xml):
|
||||
r'''
|
||||
r"""
|
||||
Render math using latex-like formatting.
|
||||
|
||||
Examples:
|
||||
@@ -37,7 +37,7 @@ class MathRenderer(object):
|
||||
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
|
||||
|
||||
TODO: use shorter tags (but this will require converting problem XML files!)
|
||||
'''
|
||||
"""
|
||||
self.system = system
|
||||
self.xml = xml
|
||||
|
||||
@@ -79,13 +79,13 @@ registry.register(MathRenderer)
|
||||
|
||||
|
||||
class SolutionRenderer(object):
|
||||
'''
|
||||
"""
|
||||
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
|
||||
extended answer (a problem "solution") after "show answers" is pressed.
|
||||
|
||||
Note that the solution content is NOT rendered and returned in the HTML. It is obtained by an
|
||||
ajax call.
|
||||
'''
|
||||
"""
|
||||
tags = ['solution']
|
||||
|
||||
def __init__(self, system, xml):
|
||||
|
||||
@@ -102,7 +102,8 @@ class Attribute(object):
|
||||
val = element.get(self.name)
|
||||
if self.default == self._sentinel and val is None:
|
||||
raise ValueError(
|
||||
'Missing required attribute {0}.'.format(self.name))
|
||||
'Missing required attribute {0}.'.format(self.name)
|
||||
)
|
||||
|
||||
if val is None:
|
||||
# not required, so return default
|
||||
@@ -156,8 +157,9 @@ class InputTypeBase(object):
|
||||
|
||||
self.input_id = state.get('id', xml.get('id'))
|
||||
if self.input_id is None:
|
||||
raise ValueError("input id state is None. xml is {0}".format(
|
||||
etree.tostring(xml)))
|
||||
raise ValueError(
|
||||
"input id state is None. xml is {0}".format(etree.tostring(xml))
|
||||
)
|
||||
|
||||
self.value = state.get('value', '')
|
||||
|
||||
@@ -259,8 +261,9 @@ class InputTypeBase(object):
|
||||
'msg': self.msg,
|
||||
'STATIC_URL': self.capa_system.STATIC_URL,
|
||||
}
|
||||
context.update((a, v) for (
|
||||
a, v) in self.loaded_attributes.iteritems() if a in self.to_render)
|
||||
context.update(
|
||||
(a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render
|
||||
)
|
||||
context.update(self._extra_context())
|
||||
return context
|
||||
|
||||
@@ -394,7 +397,7 @@ class ChoiceGroup(InputTypeBase):
|
||||
|
||||
@staticmethod
|
||||
def extract_choices(element):
|
||||
'''
|
||||
"""
|
||||
Extracts choices for a few input types, such as ChoiceGroup, RadioGroup and
|
||||
CheckboxGroup.
|
||||
|
||||
@@ -402,7 +405,7 @@ class ChoiceGroup(InputTypeBase):
|
||||
|
||||
TODO: allow order of choices to be randomized, following lon-capa spec. Use
|
||||
"location" attribute, ie random, top, bottom.
|
||||
'''
|
||||
"""
|
||||
|
||||
choices = []
|
||||
|
||||
@@ -574,7 +577,8 @@ class TextLine(InputTypeBase):
|
||||
# Preprocessor to insert between raw input and Mathjax
|
||||
self.preprocessor = {
|
||||
'class_name': self.loaded_attributes['preprocessorClassName'],
|
||||
'script_src': self.loaded_attributes['preprocessorSrc']}
|
||||
'script_src': self.loaded_attributes['preprocessorSrc'],
|
||||
}
|
||||
if None in self.preprocessor.values():
|
||||
self.preprocessor = None
|
||||
|
||||
@@ -594,9 +598,6 @@ class FileSubmission(InputTypeBase):
|
||||
template = "filesubmission.html"
|
||||
tags = ['filesubmission']
|
||||
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Your file(s) have been submitted; as soon as your submission is"
|
||||
" graded, this message will be replaced with the grader's feedback.")
|
||||
|
||||
@staticmethod
|
||||
def parse_files(files):
|
||||
@@ -618,6 +619,11 @@ class FileSubmission(InputTypeBase):
|
||||
Do some magic to handle queueing status (render as "queued" instead of "incomplete"),
|
||||
pull queue_len from the msg field. (TODO: get rid of the queue_len hack).
|
||||
"""
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
submitted_msg = _("Your file(s) have been submitted. As soon as your submission is"
|
||||
" graded, this message will be replaced with the grader's feedback.")
|
||||
self.submitted_msg = submitted_msg
|
||||
|
||||
# Check if problem has been queued
|
||||
self.queue_len = 0
|
||||
# Flag indicating that the problem has been queued, 'msg' is length of
|
||||
@@ -625,7 +631,7 @@ class FileSubmission(InputTypeBase):
|
||||
if self.status == 'incomplete':
|
||||
self.status = 'queued'
|
||||
self.queue_len = self.msg
|
||||
self.msg = FileSubmission.submitted_msg
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
def _extra_context(self):
|
||||
return {'queue_len': self.queue_len, }
|
||||
@@ -641,15 +647,13 @@ class CodeInput(InputTypeBase):
|
||||
"""
|
||||
|
||||
template = "codeinput.html"
|
||||
tags = ['codeinput',
|
||||
'textbox',
|
||||
# Another (older) name--at some point we may want to make it use a
|
||||
# non-codemirror editor.
|
||||
]
|
||||
tags = [
|
||||
'codeinput',
|
||||
'textbox',
|
||||
# Another (older) name--at some point we may want to make it use a
|
||||
# non-codemirror editor.
|
||||
]
|
||||
|
||||
# pulled out for testing
|
||||
submitted_msg = ("Submitted. As soon as your submission is"
|
||||
" graded, this message will be replaced with the grader's feedback.")
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
@@ -665,7 +669,7 @@ class CodeInput(InputTypeBase):
|
||||
Attribute('linenumbers', 'true'),
|
||||
# Template expects tabsize to be an int it can do math with
|
||||
Attribute('tabsize', 4, transform=int),
|
||||
]
|
||||
]
|
||||
|
||||
def setup_code_response_rendering(self):
|
||||
"""
|
||||
@@ -686,7 +690,12 @@ class CodeInput(InputTypeBase):
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
def setup(self):
|
||||
''' setup this input type '''
|
||||
""" setup this input type """
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
submitted_msg = _("Your answer has been submitted. As soon as your submission is"
|
||||
" graded, this message will be replaced with the grader's feedback.")
|
||||
self.submitted_msg = submitted_msg
|
||||
|
||||
self.setup_code_response_rendering()
|
||||
|
||||
def _extra_context(self):
|
||||
@@ -699,7 +708,7 @@ class CodeInput(InputTypeBase):
|
||||
|
||||
@registry.register
|
||||
class MatlabInput(CodeInput):
|
||||
'''
|
||||
"""
|
||||
InputType for handling Matlab code input
|
||||
|
||||
TODO: API_KEY will go away once we have a way to specify it per-course
|
||||
@@ -710,17 +719,20 @@ class MatlabInput(CodeInput):
|
||||
%api_key=API_KEY
|
||||
</plot_payload>
|
||||
</matlabinput>
|
||||
'''
|
||||
"""
|
||||
template = "matlabinput.html"
|
||||
tags = ['matlabinput']
|
||||
|
||||
plot_submitted_msg = ("Submitted. As soon as a response is returned, "
|
||||
"this message will be replaced by that feedback.")
|
||||
|
||||
def setup(self):
|
||||
'''
|
||||
"""
|
||||
Handle matlab-specific parsing
|
||||
'''
|
||||
"""
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
submitted_msg = _("Submitted. As soon as a response is returned, "
|
||||
"this message will be replaced by that feedback.")
|
||||
self.submitted_msg = submitted_msg
|
||||
|
||||
self.setup_code_response_rendering()
|
||||
|
||||
xml = self.xml
|
||||
@@ -736,10 +748,10 @@ class MatlabInput(CodeInput):
|
||||
if 'queuestate' in self.input_state and self.input_state['queuestate'] == 'queued':
|
||||
self.status = 'queued'
|
||||
self.queue_len = 1
|
||||
self.msg = self.plot_submitted_msg
|
||||
self.msg = self.submitted_msg
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
'''
|
||||
"""
|
||||
Handle AJAX calls directed to this input
|
||||
|
||||
Args:
|
||||
@@ -748,14 +760,14 @@ class MatlabInput(CodeInput):
|
||||
Returns:
|
||||
dict - 'success' - whether or not we successfully queued this submission
|
||||
- 'message' - message to be rendered in case of error
|
||||
'''
|
||||
"""
|
||||
|
||||
if dispatch == 'plot':
|
||||
return self._plot_data(data)
|
||||
return {}
|
||||
|
||||
def ungraded_response(self, queue_msg, queuekey):
|
||||
'''
|
||||
"""
|
||||
Handle the response from the XQueue
|
||||
Stores the response in the input_state so it can be rendered later
|
||||
|
||||
@@ -765,7 +777,7 @@ class MatlabInput(CodeInput):
|
||||
|
||||
Returns:
|
||||
nothing
|
||||
'''
|
||||
"""
|
||||
# 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):
|
||||
@@ -787,7 +799,7 @@ class MatlabInput(CodeInput):
|
||||
return True
|
||||
|
||||
def _extra_context(self):
|
||||
''' Set up additional context variables'''
|
||||
""" Set up additional context variables"""
|
||||
extra_context = {
|
||||
'queue_len': str(self.queue_len),
|
||||
'queue_msg': self.queue_msg,
|
||||
@@ -796,31 +808,31 @@ class MatlabInput(CodeInput):
|
||||
return extra_context
|
||||
|
||||
def _parse_data(self, queue_msg):
|
||||
'''
|
||||
"""
|
||||
Parses the message out of the queue message
|
||||
Args:
|
||||
queue_msg (str) - a JSON encoded string
|
||||
Returns:
|
||||
returns the value for the the key 'msg' in queue_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)
|
||||
" Received queue_msg = %s", queue_msg)
|
||||
raise
|
||||
msg = result['msg']
|
||||
return msg
|
||||
|
||||
def _plot_data(self, data):
|
||||
'''
|
||||
"""
|
||||
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
|
||||
if self.capa_system.xqueue is None:
|
||||
return {'success': False, 'message': 'Cannot connect to the queue'}
|
||||
@@ -843,11 +855,15 @@ class MatlabInput(CodeInput):
|
||||
queue_name=self.queuename)
|
||||
|
||||
# construct xqueue body
|
||||
student_info = {'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime}
|
||||
contents = {'grader_payload': self.plot_payload,
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': response}
|
||||
student_info = {
|
||||
'anonymous_student_id': anonymous_student_id,
|
||||
'submission_time': qtime
|
||||
}
|
||||
contents = {
|
||||
'grader_payload': self.plot_payload,
|
||||
'student_info': json.dumps(student_info),
|
||||
'student_response': response
|
||||
}
|
||||
|
||||
(error, msg) = qinterface.send_to_queue(header=xheader,
|
||||
body=json.dumps(contents))
|
||||
@@ -881,7 +897,8 @@ class Schematic(InputTypeBase):
|
||||
Attribute('parts', None),
|
||||
Attribute('analyses', None),
|
||||
Attribute('initial_value', None),
|
||||
Attribute('submit_analyses', None), ]
|
||||
Attribute('submit_analyses', None),
|
||||
]
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -1011,10 +1028,10 @@ class ChemicalEquationInput(InputTypeBase):
|
||||
}
|
||||
|
||||
def handle_ajax(self, dispatch, data):
|
||||
'''
|
||||
"""
|
||||
Since we only have chemcalc preview this input, check to see if it
|
||||
matches the corresponding dispatch and send it through if it does
|
||||
'''
|
||||
"""
|
||||
if dispatch == 'preview_chemcalc':
|
||||
return self.preview_chemcalc(data)
|
||||
return {}
|
||||
@@ -1097,10 +1114,10 @@ class FormulaEquationInput(InputTypeBase):
|
||||
}
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
"""
|
||||
Since we only have formcalc preview this input, check to see if it
|
||||
matches the corresponding dispatch and send it through if it does
|
||||
'''
|
||||
"""
|
||||
if dispatch == 'preview_formcalc':
|
||||
return self.preview_formcalc(get)
|
||||
return {}
|
||||
@@ -1407,7 +1424,7 @@ class AnnotationInput(InputTypeBase):
|
||||
self._validate_options()
|
||||
|
||||
def _find_options(self):
|
||||
''' Returns an array of dicts where each dict represents an option. '''
|
||||
""" Returns an array of dicts where each dict represents an option. """
|
||||
elements = self.xml.findall('./options/option')
|
||||
return [{
|
||||
'id': index,
|
||||
@@ -1416,7 +1433,7 @@ class AnnotationInput(InputTypeBase):
|
||||
} for (index, option) in enumerate(elements)]
|
||||
|
||||
def _validate_options(self):
|
||||
''' Raises a ValueError if the choice attribute is missing or invalid. '''
|
||||
""" Raises a ValueError if the choice attribute is missing or invalid. """
|
||||
valid_choices = ('correct', 'partially-correct', 'incorrect')
|
||||
for option in self.options:
|
||||
choice = option['choice']
|
||||
@@ -1427,7 +1444,7 @@ class AnnotationInput(InputTypeBase):
|
||||
choice, ', '.join(valid_choices)))
|
||||
|
||||
def _unpack(self, json_value):
|
||||
''' Unpacks the json input state into a dict. '''
|
||||
""" Unpacks the json input state into a dict. """
|
||||
d = json.loads(json_value)
|
||||
if type(d) != dict:
|
||||
d = {}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#
|
||||
# File: courseware/capa/responsetypes.py
|
||||
#
|
||||
'''
|
||||
"""
|
||||
Problem response evaluation. Handles checking of student responses,
|
||||
of a variety of types.
|
||||
|
||||
Used by capa_problem.py
|
||||
'''
|
||||
"""
|
||||
|
||||
# standard library imports
|
||||
import abc
|
||||
@@ -57,25 +57,25 @@ CORRECTMAP_PY = None
|
||||
|
||||
|
||||
class LoncapaProblemError(Exception):
|
||||
'''
|
||||
"""
|
||||
Error in specification of a problem
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResponseError(Exception):
|
||||
'''
|
||||
"""
|
||||
Error for failure in processing a response, including
|
||||
exceptions that occur when executing a custom script.
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class StudentInputError(Exception):
|
||||
'''
|
||||
"""
|
||||
Error for an invalid student input.
|
||||
For example, submitting a string when the problem expects a number
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
@@ -130,7 +130,7 @@ class LoncapaResponse(object):
|
||||
required_attributes = []
|
||||
|
||||
def __init__(self, xml, inputfields, context, system):
|
||||
'''
|
||||
"""
|
||||
Init is passed the following arguments:
|
||||
|
||||
- xml : ElementTree of this Response
|
||||
@@ -138,7 +138,7 @@ class LoncapaResponse(object):
|
||||
- context : script processor context
|
||||
- system : LoncapaSystem instance which provides OS, rendering, and user context
|
||||
|
||||
'''
|
||||
"""
|
||||
self.xml = xml
|
||||
self.inputfields = inputfields
|
||||
self.context = context
|
||||
@@ -146,6 +146,8 @@ class LoncapaResponse(object):
|
||||
|
||||
self.id = xml.get('id')
|
||||
|
||||
# The LoncapaProblemError messages here do not need to be translated as they are
|
||||
# only displayed to the user when settings.DEBUG is True
|
||||
for abox in inputfields:
|
||||
if abox.tag not in self.allowed_inputfields:
|
||||
msg = "%s: cannot have input field %s" % (
|
||||
@@ -194,20 +196,20 @@ class LoncapaResponse(object):
|
||||
self.setup_response()
|
||||
|
||||
def get_max_score(self):
|
||||
'''
|
||||
"""
|
||||
Return the total maximum points of all answer fields under this Response
|
||||
'''
|
||||
"""
|
||||
return sum(self.maxpoints.values())
|
||||
|
||||
def render_html(self, renderer, response_msg=''):
|
||||
'''
|
||||
"""
|
||||
Return XHTML Element tree representation of this Response.
|
||||
|
||||
Arguments:
|
||||
|
||||
- renderer : procedure which produces HTML given an ElementTree
|
||||
- response_msg: a message displayed at the end of the Response
|
||||
'''
|
||||
"""
|
||||
# render ourself as a <span> + our content
|
||||
tree = etree.Element('span')
|
||||
|
||||
@@ -229,12 +231,12 @@ class LoncapaResponse(object):
|
||||
return tree
|
||||
|
||||
def evaluate_answers(self, student_answers, old_cmap):
|
||||
'''
|
||||
"""
|
||||
Called by capa_problem.LoncapaProblem to evaluate student answers, and to
|
||||
generate hints (if any).
|
||||
|
||||
Returns the new CorrectMap, with (correctness,msg,hint,hintmode) for each answer_id.
|
||||
'''
|
||||
"""
|
||||
new_cmap = self.get_score(student_answers)
|
||||
self.get_hints(convert_files_to_filenames(
|
||||
student_answers), new_cmap, old_cmap)
|
||||
@@ -242,14 +244,14 @@ class LoncapaResponse(object):
|
||||
return new_cmap
|
||||
|
||||
def get_hints(self, student_answers, new_cmap, old_cmap):
|
||||
'''
|
||||
"""
|
||||
Generate adaptive hints for this problem based on student answers, the old CorrectMap,
|
||||
and the new CorrectMap produced by get_score.
|
||||
|
||||
Does not return anything.
|
||||
|
||||
Modifies new_cmap, by adding hints to answer_id entries as appropriate.
|
||||
'''
|
||||
"""
|
||||
hintgroup = self.xml.find('hintgroup')
|
||||
if hintgroup is None:
|
||||
return
|
||||
@@ -301,9 +303,10 @@ class LoncapaResponse(object):
|
||||
unsafely=self.capa_system.can_execute_unsafe_code(),
|
||||
)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating hint function %s' % (err, hintfn)
|
||||
msg += "\nSee XML source line %s" % getattr(
|
||||
self.xml, 'sourceline', '<unavailable>')
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
msg = _('Error {err} in evaluating hint function {hintfn}.').format(err=err, hintfn=hintfn)
|
||||
sourcenum = getattr(self.xml, 'sourceline', _('(Source code line unavailable)'))
|
||||
msg += "\n" + _("See XML source line {sourcenum}.").format(sourcenum=sourcenum)
|
||||
raise ResponseError(msg)
|
||||
|
||||
new_cmap.set_dict(globals_dict['new_cmap_dict'])
|
||||
@@ -346,24 +349,24 @@ class LoncapaResponse(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
"""
|
||||
Return a CorrectMap for the answers expected vs given. This includes
|
||||
(correctness, npoints, msg) for each answer_id.
|
||||
|
||||
Arguments:
|
||||
- student_answers : dict of (answer_id, answer) where answer = student input (string)
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_answers(self):
|
||||
'''
|
||||
"""
|
||||
Return a dict of (answer_id, answer_text) for each answer for this question.
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_hint_condition(self, hxml_set, student_answers):
|
||||
'''
|
||||
"""
|
||||
Return a list of hints to show.
|
||||
|
||||
- hxml_set : list of Element trees, each specifying a condition to be
|
||||
@@ -373,7 +376,7 @@ class LoncapaResponse(object):
|
||||
|
||||
Returns a list of names of hint conditions which were satisfied. Those are used
|
||||
to determine which hints are displayed.
|
||||
'''
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup_response(self):
|
||||
@@ -673,9 +676,9 @@ class ChoiceResponse(LoncapaResponse):
|
||||
'name') for choice in correct_xml])
|
||||
|
||||
def assign_choice_names(self):
|
||||
'''
|
||||
"""
|
||||
Initialize name attributes in <choice> tags for this response.
|
||||
'''
|
||||
"""
|
||||
|
||||
for index, choice in enumerate(self.xml.xpath('//*[@id=$id]//choice',
|
||||
id=self.xml.get('id'))):
|
||||
@@ -729,12 +732,13 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
self.correct_choices = [
|
||||
contextualize_text(choice.get('name'), self.context)
|
||||
for choice in cxml
|
||||
if contextualize_text(choice.get('correct'), self.context) == "true"]
|
||||
if contextualize_text(choice.get('correct'), self.context) == "true"
|
||||
]
|
||||
|
||||
def mc_setup_response(self):
|
||||
'''
|
||||
"""
|
||||
Initialize name attributes in <choice> stanzas in the <choicegroup> in this response.
|
||||
'''
|
||||
"""
|
||||
i = 0
|
||||
for response in self.xml.xpath("choicegroup"):
|
||||
rtype = response.get('type')
|
||||
@@ -749,9 +753,9 @@ class MultipleChoiceResponse(LoncapaResponse):
|
||||
choice.set("name", "choice_" + choice.get("name"))
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
"""
|
||||
grade student response.
|
||||
'''
|
||||
"""
|
||||
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
|
||||
# unicode(self), student_answers, self.correct_choices))
|
||||
if (self.answer_id in student_answers
|
||||
@@ -794,9 +798,9 @@ class TrueFalseResponse(MultipleChoiceResponse):
|
||||
|
||||
@registry.register
|
||||
class OptionResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
TODO: handle direction and randomize
|
||||
'''
|
||||
"""
|
||||
|
||||
tags = ['optionresponse']
|
||||
hint_tag = 'optionhint'
|
||||
@@ -828,10 +832,10 @@ class OptionResponse(LoncapaResponse):
|
||||
|
||||
@registry.register
|
||||
class NumericalResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
This response type expects a number or formulaic expression that evaluates
|
||||
to a number (e.g. `4+5/2^2`), and accepts with a tolerance.
|
||||
'''
|
||||
"""
|
||||
|
||||
tags = ['numericalresponse']
|
||||
hint_tag = 'numericalhint'
|
||||
@@ -877,19 +881,20 @@ class NumericalResponse(LoncapaResponse):
|
||||
log.debug("Content error--answer '%s' is not a valid number", self.correct_answer)
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
raise StudentInputError(
|
||||
_("There was a problem with the staff answer to this problem")
|
||||
_("There was a problem with the staff answer to this problem.")
|
||||
)
|
||||
|
||||
return correct_ans
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a numeric response '''
|
||||
"""Grade a numeric response"""
|
||||
student_answer = student_answers[self.answer_id]
|
||||
|
||||
correct_float = self.get_staff_ans()
|
||||
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
general_exception = StudentInputError(
|
||||
u"Could not interpret '{0}' as a number".format(cgi.escape(student_answer))
|
||||
_(u"Could not interpret '{student_answer}' as a number.").format(student_answer=cgi.escape(student_answer))
|
||||
)
|
||||
|
||||
# Begin `evaluator` block
|
||||
@@ -898,7 +903,7 @@ class NumericalResponse(LoncapaResponse):
|
||||
student_float = evaluator({}, {}, student_answer)
|
||||
except UndefinedVariable as undef_var:
|
||||
raise StudentInputError(
|
||||
u"You may not use variables ({0}) in numerical problems".format(undef_var.message)
|
||||
_(u"You may not use variables ({bad_variables}) in numerical problems.").format(bad_variables=undef_var.message)
|
||||
)
|
||||
except ValueError as val_err:
|
||||
if 'factorial' in val_err.message:
|
||||
@@ -907,14 +912,14 @@ class NumericalResponse(LoncapaResponse):
|
||||
# ve.message will be: `factorial() only accepts integral values` or
|
||||
# `factorial() not defined for negative values`
|
||||
raise StudentInputError(
|
||||
("factorial function evaluated outside its domain:"
|
||||
"'{0}'").format(cgi.escape(student_answer))
|
||||
_("factorial function evaluated outside its domain:"
|
||||
"'{student_answer}'").format(student_answer=cgi.escape(student_answer))
|
||||
)
|
||||
else:
|
||||
raise general_exception
|
||||
except ParseException:
|
||||
raise StudentInputError(
|
||||
u"Invalid math syntax: '{0}'".format(cgi.escape(student_answer))
|
||||
_(u"Invalid math syntax: '{student_answer}'").format(student_answer=cgi.escape(student_answer))
|
||||
)
|
||||
except Exception:
|
||||
raise general_exception
|
||||
@@ -957,7 +962,7 @@ class NumericalResponse(LoncapaResponse):
|
||||
|
||||
@registry.register
|
||||
class StringResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
This response type allows one or more answers.
|
||||
|
||||
Additional answers are added by `additional_answer` tag.
|
||||
@@ -987,7 +992,7 @@ class StringResponse(LoncapaResponse):
|
||||
</hintpart >
|
||||
</hintgroup>
|
||||
</stringresponse>
|
||||
'''
|
||||
"""
|
||||
tags = ['stringresponse']
|
||||
hint_tag = 'stringhint'
|
||||
allowed_inputfields = ['textline']
|
||||
@@ -1020,7 +1025,7 @@ class StringResponse(LoncapaResponse):
|
||||
self.xml.remove(el)
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''Grade a string response '''
|
||||
"""Grade a string response """
|
||||
student_answer = student_answers[self.answer_id].strip()
|
||||
correct = self.check_string(self.correct_answer, student_answer)
|
||||
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
|
||||
@@ -1092,10 +1097,10 @@ class StringResponse(LoncapaResponse):
|
||||
|
||||
@registry.register
|
||||
class CustomResponse(LoncapaResponse):
|
||||
'''
|
||||
"""
|
||||
Custom response. The python code to be run should be in <answer>...</answer>
|
||||
or in a <script>...</script>
|
||||
'''
|
||||
"""
|
||||
|
||||
tags = ['customresponse']
|
||||
|
||||
@@ -1176,10 +1181,10 @@ class CustomResponse(LoncapaResponse):
|
||||
self.code = answer.text
|
||||
|
||||
def get_score(self, student_answers):
|
||||
'''
|
||||
"""
|
||||
student_answers is a dict with everything from request.POST, but with the first part
|
||||
of each key removed (the string before the first "_").
|
||||
'''
|
||||
"""
|
||||
|
||||
log.debug('%s: student_answers=%s', unicode(self), student_answers)
|
||||
|
||||
@@ -1345,8 +1350,10 @@ class CustomResponse(LoncapaResponse):
|
||||
# Raise an exception
|
||||
else:
|
||||
log.error(traceback.format_exc())
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
raise ResponseError(
|
||||
"CustomResponse: check function returned an invalid dict")
|
||||
_("CustomResponse: check function returned an invalid dictionary!")
|
||||
)
|
||||
|
||||
else:
|
||||
correct = ['correct' if ret else 'incorrect'] * len(idset)
|
||||
@@ -1385,7 +1392,7 @@ class CustomResponse(LoncapaResponse):
|
||||
return ""
|
||||
|
||||
def get_answers(self):
|
||||
'''
|
||||
"""
|
||||
Give correct answer expected for this response.
|
||||
|
||||
use default_answer_map from entry elements (eg textline),
|
||||
@@ -1393,7 +1400,7 @@ class CustomResponse(LoncapaResponse):
|
||||
|
||||
but for simplicity, if an "expect" attribute was given by the content author
|
||||
ie <customresponse expect="foo" ...> then that.
|
||||
'''
|
||||
"""
|
||||
if len(self.answer_ids) > 1:
|
||||
return self.default_answer_map
|
||||
if self.expect:
|
||||
@@ -1401,12 +1408,12 @@ class CustomResponse(LoncapaResponse):
|
||||
return self.default_answer_map
|
||||
|
||||
def _handle_exec_exception(self, err):
|
||||
'''
|
||||
"""
|
||||
Handle an exception raised during the execution of
|
||||
custom Python code.
|
||||
|
||||
Raises a ResponseError
|
||||
'''
|
||||
"""
|
||||
|
||||
# Log the error if we are debugging
|
||||
msg = 'Error occurred while evaluating CustomResponse'
|
||||
@@ -1498,11 +1505,11 @@ class CodeResponse(LoncapaResponse):
|
||||
queue_name = None
|
||||
|
||||
def setup_response(self):
|
||||
'''
|
||||
"""
|
||||
Configure CodeResponse from XML. Supports both CodeResponse and ExternalResponse XML
|
||||
|
||||
TODO: Determines whether in synchronous or asynchronous (queued) mode
|
||||
'''
|
||||
"""
|
||||
xml = self.xml
|
||||
# TODO: XML can override external resource (grader/queue) URL
|
||||
self.url = xml.get('url', None)
|
||||
@@ -1522,12 +1529,12 @@ class CodeResponse(LoncapaResponse):
|
||||
self._parse_coderesponse_xml(codeparam)
|
||||
|
||||
def _parse_coderesponse_xml(self, codeparam):
|
||||
'''
|
||||
"""
|
||||
Parse the new CodeResponse XML format. When successful, sets:
|
||||
self.initial_display
|
||||
self.answer (an answer to display to the student in the LMS)
|
||||
self.payload
|
||||
'''
|
||||
"""
|
||||
# Note that CodeResponse is agnostic to the specific contents of
|
||||
# grader_payload
|
||||
grader_payload = codeparam.find('grader_payload')
|
||||
@@ -1614,9 +1621,10 @@ class CodeResponse(LoncapaResponse):
|
||||
|
||||
cmap = CorrectMap()
|
||||
if error:
|
||||
cmap.set(self.answer_id, queuestate=None,
|
||||
msg='Unable to deliver your submission to grader. (Reason: %s.)'
|
||||
' Please try again later.' % msg)
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
error_msg = _('Unable to deliver your submission to grader (Reason: {error_msg}).'
|
||||
' Please try again later.').format(error_msg=msg)
|
||||
cmap.set(self.answer_id, queuestate=None, msg=error_msg)
|
||||
else:
|
||||
# Queueing mechanism flags:
|
||||
# 1) Backend: Non-null CorrectMap['queuestate'] indicates that
|
||||
@@ -1632,9 +1640,13 @@ class CodeResponse(LoncapaResponse):
|
||||
def update_score(self, score_msg, oldcmap, queuekey):
|
||||
"""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)
|
||||
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
|
||||
if not valid_score_msg:
|
||||
oldcmap.set(self.answer_id,
|
||||
msg='Invalid grader reply. Please contact the course staff.')
|
||||
# Translators: 'grader' refers to the edX automatic code grader.
|
||||
error_msg = _('Invalid grader reply. Please contact the course staff.')
|
||||
oldcmap.set(self.answer_id, msg=error_msg)
|
||||
return oldcmap
|
||||
|
||||
correctness = 'correct' if correct else 'incorrect'
|
||||
@@ -1944,6 +1956,8 @@ class FormulaResponse(LoncapaResponse):
|
||||
Each dictionary represents a test case for the answer.
|
||||
Returns a tuple of formula evaluation results.
|
||||
"""
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
|
||||
out = []
|
||||
for var_dict in var_dict_list:
|
||||
try:
|
||||
@@ -1959,7 +1973,7 @@ class FormulaResponse(LoncapaResponse):
|
||||
cgi.escape(answer)
|
||||
)
|
||||
raise StudentInputError(
|
||||
"Invalid input: " + err.message + " not permitted in answer"
|
||||
_("Invalid input: {bad_input} not permitted in answer.").format(bad_input=err.message)
|
||||
)
|
||||
except ValueError as err:
|
||||
if 'factorial' in err.message:
|
||||
@@ -1974,19 +1988,25 @@ class FormulaResponse(LoncapaResponse):
|
||||
cgi.escape(answer)
|
||||
)
|
||||
raise StudentInputError(
|
||||
("factorial function not permitted in answer "
|
||||
"for this problem. Provided answer was: "
|
||||
"{0}").format(cgi.escape(answer))
|
||||
_("factorial function not permitted in answer "
|
||||
"for this problem. Provided answer was: "
|
||||
"{bad_input}").format(bad_input=cgi.escape(answer))
|
||||
)
|
||||
# If non-factorial related ValueError thrown, handle it the same as any other Exception
|
||||
log.debug('formularesponse: error %s in formula', err)
|
||||
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
|
||||
cgi.escape(answer))
|
||||
raise StudentInputError(
|
||||
_("Invalid input: Could not parse '{bad_input}' as a formula.").format(
|
||||
bad_input=cgi.escape(answer)
|
||||
)
|
||||
)
|
||||
except Exception as err:
|
||||
# traceback.print_exc()
|
||||
log.debug('formularesponse: error %s in formula', err)
|
||||
raise StudentInputError("Invalid input: Could not parse '%s' as a formula" %
|
||||
cgi.escape(answer))
|
||||
raise StudentInputError(
|
||||
_("Invalid input: Could not parse '{bad_input}' as a formula").format(
|
||||
bad_input=cgi.escape(answer)
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
def randomize_variables(self, samples):
|
||||
@@ -2124,7 +2144,9 @@ class SchematicResponse(LoncapaResponse):
|
||||
unsafely=self.capa_system.can_execute_unsafe_code(),
|
||||
)
|
||||
except Exception as err:
|
||||
msg = 'Error %s in evaluating SchematicResponse' % err
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
# Translators: 'SchematicResponse' is a problem type and should not be translated.
|
||||
msg = _('Error in evaluating SchematicResponse. The error was: {error_msg}').format(error_msg=err)
|
||||
raise ResponseError(msg)
|
||||
cmap = CorrectMap()
|
||||
cmap.set_dict(dict(zip(sorted(self.answer_ids), self.context['correct'])))
|
||||
@@ -2674,6 +2696,7 @@ class ChoiceTextResponse(LoncapaResponse):
|
||||
|
||||
Returns True if and only if all student inputs are correct.
|
||||
"""
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
inputs_correct = True
|
||||
for answer_name, answer_value in numtolerance_inputs.iteritems():
|
||||
# If `self.corrrect_inputs` does not contain an entry for
|
||||
@@ -2691,11 +2714,11 @@ class ChoiceTextResponse(LoncapaResponse):
|
||||
correct_ans = complex(correct_ans)
|
||||
except ValueError:
|
||||
log.debug(
|
||||
"Content error--answer" +
|
||||
"'{0}' is not a valid complex number".format(correct_ans)
|
||||
"Content error--answer '%s' is not a valid complex number",
|
||||
correct_ans
|
||||
)
|
||||
raise StudentInputError(
|
||||
"The Staff answer could not be interpreted as a number."
|
||||
_("The Staff answer could not be interpreted as a number.")
|
||||
)
|
||||
# Compare the student answer to the staff answer/ or to 0
|
||||
# if all that is important is verifying numericality
|
||||
@@ -2708,14 +2731,13 @@ class ChoiceTextResponse(LoncapaResponse):
|
||||
except:
|
||||
# Use the traceback-preserving version of re-raising with a
|
||||
# different type
|
||||
_, _, trace = sys.exc_info()
|
||||
|
||||
raise StudentInputError(
|
||||
"Could not interpret '{0}' as a number{1}".format(
|
||||
cgi.escape(answer_value),
|
||||
trace
|
||||
)
|
||||
__, __, trace = sys.exc_info()
|
||||
msg = _("Could not interpret '{given_answer}' as a number.").format(
|
||||
given_answer=cgi.escape(answer_value)
|
||||
)
|
||||
msg += " ({0})".format(trace)
|
||||
raise StudentInputError(msg)
|
||||
|
||||
# Ignore the results of the comparisons which were just for
|
||||
# Numerical Validation.
|
||||
if answer_name in self.correct_inputs and not partial_correct:
|
||||
|
||||
@@ -313,7 +313,7 @@ class FileSubmissionTest(unittest.TestCase):
|
||||
'STATIC_URL': '/dummy-static/',
|
||||
'id': 'prob_1_2',
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'msg': the_input.submitted_msg,
|
||||
'value': 'BumbleBee.py',
|
||||
'queue_len': '3',
|
||||
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
|
||||
@@ -362,7 +362,7 @@ class CodeInputTest(unittest.TestCase):
|
||||
'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': input_class.submitted_msg,
|
||||
'msg': the_input.submitted_msg,
|
||||
'mode': mode,
|
||||
'linenumbers': linenumbers,
|
||||
'rows': rows,
|
||||
@@ -415,7 +415,7 @@ class MatlabTest(unittest.TestCase):
|
||||
'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'msg': self.the_input.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
@@ -444,7 +444,7 @@ class MatlabTest(unittest.TestCase):
|
||||
'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.submitted_msg,
|
||||
'msg': the_input.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
@@ -501,7 +501,7 @@ class MatlabTest(unittest.TestCase):
|
||||
'id': 'prob_1_2',
|
||||
'value': 'print "good evening"',
|
||||
'status': 'queued',
|
||||
'msg': self.input_class.plot_submitted_msg,
|
||||
'msg': the_input.submitted_msg,
|
||||
'mode': self.mode,
|
||||
'rows': self.rows,
|
||||
'cols': self.cols,
|
||||
|
||||
@@ -1150,7 +1150,7 @@ class NumericalResponseTest(ResponseTest):
|
||||
"""A fake gettext.Translations object."""
|
||||
def ugettext(self, text):
|
||||
"""Return the 'translation' of `text`."""
|
||||
if text == "There was a problem with the staff answer to this problem":
|
||||
if text == "There was a problem with the staff answer to this problem.":
|
||||
text = "TRANSLATED!"
|
||||
return text
|
||||
problem.capa_system.i18n = FakeTranslations()
|
||||
|
||||
@@ -8,10 +8,10 @@ default_tolerance = '0.001%'
|
||||
|
||||
|
||||
def compare_with_tolerance(v1, v2, tol=default_tolerance):
|
||||
'''
|
||||
"""
|
||||
Compare v1 to v2 with maximum tolerance tol.
|
||||
|
||||
tol is relative if it ends in %; otherwise, it is absolute
|
||||
tol is relative if it ends in %; otherwise, it is absolute.
|
||||
|
||||
- v1 : student result (float complex number)
|
||||
- v2 : instructor result (float complex number)
|
||||
@@ -26,7 +26,7 @@ def compare_with_tolerance(v1, v2, tol=default_tolerance):
|
||||
Out[183]: -3.3881317890172014e-21
|
||||
In [212]: 1.9e24 - 1.9*10**24
|
||||
Out[212]: 268435456.0
|
||||
'''
|
||||
"""
|
||||
relative = tol.endswith('%')
|
||||
if relative:
|
||||
tolerance_rel = evaluator(dict(), dict(), tol[:-1]) * 0.01
|
||||
@@ -46,8 +46,10 @@ def compare_with_tolerance(v1, v2, tol=default_tolerance):
|
||||
|
||||
|
||||
def contextualize_text(text, context): # private
|
||||
''' Takes a string with variables. E.g. $a+$b.
|
||||
Does a substitution of those variables from the context '''
|
||||
"""
|
||||
Takes a string with variables. E.g. $a+$b.
|
||||
Does a substitution of those variables from the context
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
for key in sorted(context, lambda x, y: cmp(len(y), len(x))):
|
||||
@@ -66,10 +68,10 @@ def contextualize_text(text, context): # private
|
||||
|
||||
|
||||
def convert_files_to_filenames(answers):
|
||||
'''
|
||||
"""
|
||||
Check for File objects in the dict of submitted answers,
|
||||
convert File objects to their filename (string)
|
||||
'''
|
||||
"""
|
||||
new_answers = dict()
|
||||
for answer_id in answers.keys():
|
||||
answer = answers[answer_id]
|
||||
@@ -86,9 +88,9 @@ def is_list_of_files(files):
|
||||
|
||||
|
||||
def is_file(file_to_test):
|
||||
'''
|
||||
"""
|
||||
Duck typing to check if 'file_to_test' is a File object
|
||||
'''
|
||||
"""
|
||||
return all(hasattr(file_to_test, method) for method in ['read', 'name'])
|
||||
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ dateformat = '%Y%m%d%H%M%S'
|
||||
|
||||
|
||||
def make_hashkey(seed):
|
||||
'''
|
||||
"""
|
||||
Generate a string key by hashing
|
||||
'''
|
||||
"""
|
||||
h = hashlib.md5()
|
||||
h.update(str(seed))
|
||||
return h.hexdigest()
|
||||
@@ -57,9 +57,9 @@ def parse_xreply(xreply):
|
||||
|
||||
|
||||
class XQueueInterface(object):
|
||||
'''
|
||||
"""
|
||||
Interface to the external grading system
|
||||
'''
|
||||
"""
|
||||
|
||||
def __init__(self, url, django_auth, requests_auth=None):
|
||||
self.url = url
|
||||
@@ -106,8 +106,10 @@ class XQueueInterface(object):
|
||||
return self._http_post(self.url + '/xqueue/login/', payload)
|
||||
|
||||
def _send_to_queue(self, header, body, files_to_upload):
|
||||
payload = {'xqueue_header': header,
|
||||
'xqueue_body': body}
|
||||
payload = {
|
||||
'xqueue_header': header,
|
||||
'xqueue_body': body
|
||||
}
|
||||
files = {}
|
||||
if files_to_upload is not None:
|
||||
for f in files_to_upload:
|
||||
|
||||
@@ -492,27 +492,31 @@ class CapaMixin(CapaFields):
|
||||
if answer_id.find(hidden_state_keyword) >= 0:
|
||||
student_answers.pop(answer_id)
|
||||
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
# Next, generate a fresh LoncapaProblem
|
||||
self.lcp = self.new_lcp(None)
|
||||
self.set_state_from_lcp()
|
||||
|
||||
# Prepend a scary warning to the student
|
||||
warning = '<div class="capa_reset">'\
|
||||
'<h2>Warning: The problem has been reset to its initial state!</h2>'\
|
||||
'The problem\'s state was corrupted by an invalid submission. ' \
|
||||
'The submission consisted of:'\
|
||||
'<ul>'
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
warning_msg = _("Warning: The problem has been reset to its initial state!")
|
||||
warning = '<div class="capa_reset"> <h2> ' + warning_msg + '</h2>'
|
||||
|
||||
# Translators: Following this message, there will be a bulleted list of items.
|
||||
warning_msg = _("The problem's state was corrupted by an invalid submission. The submission consisted of:")
|
||||
warning += warning_msg + '<ul>'
|
||||
|
||||
for student_answer in student_answers.values():
|
||||
if student_answer != '':
|
||||
warning += '<li>' + cgi.escape(student_answer) + '</li>'
|
||||
warning += '</ul>'\
|
||||
'If this error persists, please contact the course staff.'\
|
||||
'</div>'
|
||||
|
||||
warning_msg = _('If this error persists, please contact the course staff.')
|
||||
warning += '</ul>' + warning_msg + '</div>'
|
||||
|
||||
html = warning
|
||||
try:
|
||||
html += self.lcp.get_html()
|
||||
except Exception: # Couldn't do it. Give up
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Couldn't do it. Give up.
|
||||
log.exception("Unable to generate html from LoncapaProblem")
|
||||
raise
|
||||
|
||||
@@ -541,20 +545,22 @@ class CapaMixin(CapaFields):
|
||||
else:
|
||||
check_button = False
|
||||
|
||||
content = {'name': self.display_name_with_default,
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
}
|
||||
content = {
|
||||
'name': self.display_name_with_default,
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
}
|
||||
|
||||
context = {'problem': content,
|
||||
'id': self.id,
|
||||
'check_button': check_button,
|
||||
'reset_button': self.should_show_reset_button(),
|
||||
'save_button': self.should_show_save_button(),
|
||||
'answer_available': self.answer_available(),
|
||||
'attempts_used': self.attempts,
|
||||
'attempts_allowed': self.max_attempts,
|
||||
}
|
||||
context = {
|
||||
'problem': content,
|
||||
'id': self.id,
|
||||
'check_button': check_button,
|
||||
'reset_button': self.should_show_reset_button(),
|
||||
'save_button': self.should_show_save_button(),
|
||||
'answer_available': self.answer_available(),
|
||||
'attempts_used': self.attempts,
|
||||
'attempts_allowed': self.max_attempts,
|
||||
}
|
||||
|
||||
html = self.runtime.render_template('problem.html', context)
|
||||
|
||||
@@ -563,7 +569,7 @@ class CapaMixin(CapaFields):
|
||||
id=self.location.html_id(), ajax_url=self.runtime.ajax_url
|
||||
) + html + "</div>"
|
||||
|
||||
# now do all the substitutions which the LMS module_render normally does, but
|
||||
# Now do all the substitutions which the LMS module_render normally does, but
|
||||
# we need to do here explicitly since we can get called for our HTML via AJAX
|
||||
html = self.runtime.replace_urls(html)
|
||||
if self.runtime.replace_course_urls:
|
||||
@@ -855,17 +861,19 @@ class CapaMixin(CapaFields):
|
||||
answers = self.make_dict_of_responses(data)
|
||||
event_info['answers'] = convert_files_to_filenames(answers)
|
||||
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.runtime.track_function('problem_check_fail', event_info)
|
||||
raise NotFoundError('Problem is closed')
|
||||
raise NotFoundError(_("Problem is closed."))
|
||||
|
||||
# Problem submitted. Student should reset before checking again
|
||||
if self.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'unreset'
|
||||
self.runtime.track_function('problem_check_fail', event_info)
|
||||
raise NotFoundError('Problem must be reset before it can be checked again')
|
||||
raise NotFoundError(_("Problem must be reset before it can be checked again."))
|
||||
|
||||
# Problem queued. Students must wait a specified waittime before they are allowed to submit
|
||||
if self.lcp.is_queued():
|
||||
@@ -873,7 +881,7 @@ class CapaMixin(CapaFields):
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
waittime_between_requests = self.runtime.xqueue['waittime']
|
||||
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
msg = u'You must wait at least {wait} seconds between submissions'.format(
|
||||
msg = _(u"You must wait at least {wait} seconds between submissions.").format(
|
||||
wait=waittime_between_requests)
|
||||
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
|
||||
|
||||
@@ -899,7 +907,8 @@ class CapaMixin(CapaFields):
|
||||
# Otherwise, display just an error message,
|
||||
# without a stack trace
|
||||
else:
|
||||
msg = u"Error: {msg}".format(msg=inst.message)
|
||||
# Translators: {msg} will be replaced with a problem's error message.
|
||||
msg = _(u"Error: {msg}").format(msg=inst.message)
|
||||
|
||||
return {'success': msg}
|
||||
|
||||
@@ -936,9 +945,10 @@ class CapaMixin(CapaFields):
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
return {'success': success,
|
||||
'contents': html,
|
||||
}
|
||||
return {
|
||||
'success': success,
|
||||
'contents': html,
|
||||
}
|
||||
|
||||
def rescore_problem(self):
|
||||
"""
|
||||
@@ -958,15 +968,18 @@ class CapaMixin(CapaFields):
|
||||
"""
|
||||
event_info = {'state': self.lcp.get_state(), 'problem_id': self.location.url()}
|
||||
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
if not self.lcp.supports_rescoring():
|
||||
event_info['failure'] = 'unsupported'
|
||||
self.runtime.track_function('problem_rescore_fail', event_info)
|
||||
raise NotImplementedError("Problem's definition does not support rescoring")
|
||||
# Translators: 'rescoring' refers to the act of re-submitting a student's solution so it can get a new score.
|
||||
raise NotImplementedError(_("Problem's definition does not support rescoring."))
|
||||
|
||||
if not self.done:
|
||||
event_info['failure'] = 'unanswered'
|
||||
self.runtime.track_function('problem_rescore_fail', event_info)
|
||||
raise NotFoundError('Problem must be answered before it can be graded again')
|
||||
raise NotFoundError(_("Problem must be answered before it can be graded again."))
|
||||
|
||||
# get old score, for comparison:
|
||||
orig_score = self.lcp.get_score()
|
||||
@@ -1032,32 +1045,40 @@ class CapaMixin(CapaFields):
|
||||
|
||||
answers = self.make_dict_of_responses(data)
|
||||
event_info['answers'] = answers
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
# Too late. Cannot submit
|
||||
if self.closed() and not self.max_attempts == 0:
|
||||
event_info['failure'] = 'closed'
|
||||
self.runtime.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'msg': "Problem is closed"}
|
||||
return {
|
||||
'success': False,
|
||||
# Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem.
|
||||
'msg': _("Problem is closed.")
|
||||
}
|
||||
|
||||
# Problem submitted. Student should reset before saving
|
||||
# again.
|
||||
if self.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'done'
|
||||
self.runtime.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'msg': "Problem needs to be reset prior to save"}
|
||||
return {
|
||||
'success': False,
|
||||
'msg': _("Problem needs to be reset prior to save.")
|
||||
}
|
||||
|
||||
self.lcp.student_answers = answers
|
||||
|
||||
self.set_state_from_lcp()
|
||||
|
||||
self.runtime.track_function('save_problem_success', event_info)
|
||||
msg = "Your answers have been saved"
|
||||
msg = _("Your answers have been saved.")
|
||||
if not self.max_attempts == 0:
|
||||
msg += " but not graded. Hit 'Check' to grade them."
|
||||
return {'success': True,
|
||||
'msg': msg}
|
||||
msg = _("Your answers have been saved but not graded. Click 'Check' to grade them.")
|
||||
return {
|
||||
'success': True,
|
||||
'msg': msg,
|
||||
}
|
||||
|
||||
def reset_problem(self, _data):
|
||||
"""
|
||||
@@ -1074,18 +1095,24 @@ class CapaMixin(CapaFields):
|
||||
event_info = dict()
|
||||
event_info['old_state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.runtime.track_function('reset_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Problem is closed"}
|
||||
return {
|
||||
'success': False,
|
||||
# Translators: 'closed' means the problem's due date has passed. You may no longer attempt to solve the problem.
|
||||
'error': _("Problem is closed."),
|
||||
}
|
||||
|
||||
if not self.done:
|
||||
event_info['failure'] = 'not_done'
|
||||
self.runtime.track_function('reset_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Refresh the page and make an attempt before resetting."}
|
||||
return {
|
||||
'success': False,
|
||||
'error': _("Refresh the page and make an attempt before resetting."),
|
||||
}
|
||||
|
||||
if self.rerandomize in ["always", "onreset"]:
|
||||
# Reset random number generator seed.
|
||||
@@ -1100,5 +1127,7 @@ class CapaMixin(CapaFields):
|
||||
event_info['new_state'] = self.lcp.get_state()
|
||||
self.runtime.track_function('reset_problem', event_info)
|
||||
|
||||
return {'success': True,
|
||||
'html': self.get_problem_html(encapsulate=False)}
|
||||
return {
|
||||
'success': True,
|
||||
'html': self.get_problem_html(encapsulate=False),
|
||||
}
|
||||
|
||||
@@ -62,12 +62,14 @@ class CapaModule(CapaMixin, XModule):
|
||||
'ungraded_response': self.handle_ungraded_response
|
||||
}
|
||||
|
||||
generic_error_message = (
|
||||
_ = self.runtime.service(self, "i18n").ugettext
|
||||
|
||||
generic_error_message = _(
|
||||
"We're sorry, there was an error with processing your request. "
|
||||
"Please try reloading your page and trying again."
|
||||
)
|
||||
|
||||
not_found_error_message = (
|
||||
not_found_error_message = _(
|
||||
"The state of this problem has changed since you loaded this page. "
|
||||
"Please refresh your page."
|
||||
)
|
||||
|
||||
@@ -265,10 +265,10 @@ class TestRescoringTask(TestIntegrationTask):
|
||||
self.assertEqual(instructor_task.task_state, FAILURE)
|
||||
status = json.loads(instructor_task.task_output)
|
||||
self.assertEqual(status['exception'], 'NotImplementedError')
|
||||
self.assertEqual(status['message'], "Problem's definition does not support rescoring")
|
||||
self.assertEqual(status['message'], "Problem's definition does not support rescoring.")
|
||||
|
||||
status = InstructorTaskModuleTestCase.get_task_status(instructor_task.task_id)
|
||||
self.assertEqual(status['message'], "Problem's definition does not support rescoring")
|
||||
self.assertEqual(status['message'], "Problem's definition does not support rescoring.")
|
||||
|
||||
def define_randomized_custom_response_problem(self, problem_url_name, redefine=False):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user