Merge pull request #203 from edx/pbaratta/fix/capa-unicode
Fix unicode for errors on capa problems
This commit is contained in:
@@ -93,7 +93,7 @@ def check_variables(string, variables):
|
||||
Pyparsing uses a left-to-right parser, which makes a more
|
||||
elegant approach pretty hopeless.
|
||||
"""
|
||||
general_whitespace = re.compile('[^\\w]+')
|
||||
general_whitespace = re.compile('[^\\w]+') # TODO consider non-ascii
|
||||
# List of all alnums in string
|
||||
possible_variables = re.split(general_whitespace, string)
|
||||
bad_variables = []
|
||||
|
||||
@@ -47,6 +47,9 @@ def randomization_bin(seed, problem_id):
|
||||
|
||||
|
||||
class Randomization(String):
|
||||
"""
|
||||
Define a field to store how to randomize a problem.
|
||||
"""
|
||||
def from_json(self, value):
|
||||
if value in ("", "true"):
|
||||
return "always"
|
||||
@@ -58,24 +61,39 @@ class Randomization(String):
|
||||
|
||||
|
||||
class ComplexEncoder(json.JSONEncoder):
|
||||
"""
|
||||
Extend the JSON encoder to correctly handle complex numbers
|
||||
"""
|
||||
def default(self, obj):
|
||||
"""
|
||||
Print a nicely formatted complex number, or default to the JSON encoder
|
||||
"""
|
||||
if isinstance(obj, complex):
|
||||
return "{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
|
||||
return u"{real:.7g}{imag:+.7g}*j".format(real=obj.real, imag=obj.imag)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class CapaFields(object):
|
||||
attempts = Integer(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
"""
|
||||
Define the possible fields for a Capa problem
|
||||
"""
|
||||
attempts = Integer(help="Number of attempts taken by the student on this problem",
|
||||
default=0, scope=Scope.user_state)
|
||||
max_attempts = Integer(
|
||||
display_name="Maximum Attempts",
|
||||
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
|
||||
help=("Defines the number of times a student can try to answer this problem. "
|
||||
"If the value is not set, infinite attempts are allowed."),
|
||||
values={"min": 0}, scope=Scope.settings
|
||||
)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
|
||||
graceperiod = Timedelta(
|
||||
help="Amount of time after the due date that submissions will be accepted",
|
||||
scope=Scope.settings
|
||||
)
|
||||
showanswer = String(
|
||||
display_name="Show Answer",
|
||||
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
|
||||
help=("Defines when to show the answer to the problem. "
|
||||
"A default value can be set in Advanced Settings."),
|
||||
scope=Scope.settings, default="closed",
|
||||
values=[
|
||||
{"display_name": "Always", "value": "always"},
|
||||
@@ -86,23 +104,33 @@ class CapaFields(object):
|
||||
{"display_name": "Past Due", "value": "past_due"},
|
||||
{"display_name": "Never", "value": "never"}]
|
||||
)
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
force_save_button = Boolean(
|
||||
help="Whether to force the save button to appear on the page",
|
||||
scope=Scope.settings, default=False
|
||||
)
|
||||
rerandomize = Randomization(
|
||||
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
|
||||
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "On Reset", "value": "onreset"},
|
||||
{"display_name": "Never", "value": "never"},
|
||||
{"display_name": "Per Student", "value": "per_student"}]
|
||||
display_name="Randomization",
|
||||
help="Defines how often inputs are randomized when a student loads the problem. "
|
||||
"This setting only applies to problems that can have randomly generated numeric values. "
|
||||
"A default value can be set in Advanced Settings.",
|
||||
default="always", scope=Scope.settings, values=[
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "On Reset", "value": "onreset"},
|
||||
{"display_name": "Never", "value": "never"},
|
||||
{"display_name": "Per Student", "value": "per_student"}
|
||||
]
|
||||
)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Dict(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
correct_map = Dict(help="Dictionary with the correctness of current student answers",
|
||||
scope=Scope.user_state, default={})
|
||||
input_state = Dict(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Dict(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
seed = Integer(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = Float(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
|
||||
help=("Defines the number of points each problem is worth. "
|
||||
"If the value is not set, each response field in the problem is worth one point."),
|
||||
values={"min": 0, "step": .1},
|
||||
scope=Scope.settings
|
||||
)
|
||||
@@ -114,12 +142,12 @@ class CapaFields(object):
|
||||
|
||||
|
||||
class CapaModule(CapaFields, XModule):
|
||||
'''
|
||||
"""
|
||||
An XModule implementing LonCapa format problems, implemented by way of
|
||||
capa.capa_problem.LoncapaProblem
|
||||
|
||||
CapaModule.__init__ takes the same arguments as xmodule.x_module:XModule.__init__
|
||||
'''
|
||||
"""
|
||||
icon_class = 'problem'
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
|
||||
@@ -134,7 +162,9 @@ class CapaModule(CapaFields, XModule):
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Accepts the same arguments as xmodule.x_module:XModule.__init__ """
|
||||
"""
|
||||
Accepts the same arguments as xmodule.x_module:XModule.__init__
|
||||
"""
|
||||
XModule.__init__(self, *args, **kwargs)
|
||||
|
||||
due_date = self.due
|
||||
@@ -167,7 +197,7 @@ class CapaModule(CapaFields, XModule):
|
||||
self.seed = self.lcp.seed
|
||||
|
||||
except Exception as err:
|
||||
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
|
||||
msg = u'cannot create LoncapaProblem {loc}: {err}'.format(
|
||||
loc=self.location.url(), err=err)
|
||||
# TODO (vshnayder): do modules need error handlers too?
|
||||
# We shouldn't be switching on DEBUG.
|
||||
@@ -176,12 +206,15 @@ class CapaModule(CapaFields, XModule):
|
||||
# TODO (vshnayder): This logic should be general, not here--and may
|
||||
# want to preserve the data instead of replacing it.
|
||||
# e.g. in the CMS
|
||||
msg = '<p>%s</p>' % msg.replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
msg = u'<p>{msg}</p>'.format(msg=cgi.escape(msg))
|
||||
msg += u'<p><pre>{tb}</pre></p>'.format(
|
||||
tb=cgi.escape(traceback.format_exc()))
|
||||
# create a dummy problem with error message instead of failing
|
||||
problem_text = ('<problem><text><span class="inline-error">'
|
||||
'Problem %s has an error:</span>%s</text></problem>' %
|
||||
(self.location.url(), msg))
|
||||
problem_text = (u'<problem><text><span class="inline-error">'
|
||||
u'Problem {url} has an error:</span>{msg}</text></problem>'.format(
|
||||
url=self.location.url(),
|
||||
msg=msg)
|
||||
)
|
||||
self.lcp = self.new_lcp(self.get_state_for_lcp(), text=problem_text)
|
||||
else:
|
||||
# add extra info and raise
|
||||
@@ -192,7 +225,9 @@ class CapaModule(CapaFields, XModule):
|
||||
assert self.seed is not None
|
||||
|
||||
def choose_new_seed(self):
|
||||
"""Choose a new seed."""
|
||||
"""
|
||||
Choose a new seed.
|
||||
"""
|
||||
if self.rerandomize == 'never':
|
||||
self.seed = 1
|
||||
elif self.rerandomize == "per_student" and hasattr(self.system, 'seed'):
|
||||
@@ -206,6 +241,9 @@ class CapaModule(CapaFields, XModule):
|
||||
self.seed %= MAX_RANDOMIZATION_BINS
|
||||
|
||||
def new_lcp(self, state, text=None):
|
||||
"""
|
||||
Generate a new Loncapa Problem
|
||||
"""
|
||||
if text is None:
|
||||
text = self.data
|
||||
|
||||
@@ -218,6 +256,9 @@ class CapaModule(CapaFields, XModule):
|
||||
)
|
||||
|
||||
def get_state_for_lcp(self):
|
||||
"""
|
||||
Give a dictionary holding the state of the module
|
||||
"""
|
||||
return {
|
||||
'done': self.done,
|
||||
'correct_map': self.correct_map,
|
||||
@@ -227,6 +268,9 @@ class CapaModule(CapaFields, XModule):
|
||||
}
|
||||
|
||||
def set_state_from_lcp(self):
|
||||
"""
|
||||
Set the module's state from the settings in `self.lcp`
|
||||
"""
|
||||
lcp_state = self.lcp.get_state()
|
||||
self.done = lcp_state['done']
|
||||
self.correct_map = lcp_state['correct_map']
|
||||
@@ -235,26 +279,36 @@ class CapaModule(CapaFields, XModule):
|
||||
self.seed = lcp_state['seed']
|
||||
|
||||
def get_score(self):
|
||||
"""
|
||||
Access the problem's score
|
||||
"""
|
||||
return self.lcp.get_score()
|
||||
|
||||
def max_score(self):
|
||||
"""
|
||||
Access the problem's max score
|
||||
"""
|
||||
return self.lcp.get_max_score()
|
||||
|
||||
def get_progress(self):
|
||||
''' For now, just return score / max_score
|
||||
'''
|
||||
"""
|
||||
For now, just return score / max_score
|
||||
"""
|
||||
d = self.get_score()
|
||||
score = d['score']
|
||||
total = d['total']
|
||||
if total > 0:
|
||||
try:
|
||||
return Progress(score, total)
|
||||
except Exception:
|
||||
except (TypeError, ValueError):
|
||||
log.exception("Got bad progress")
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Return some html with data about the module
|
||||
"""
|
||||
return self.system.render_template('problem_ajax.html', {
|
||||
'element_id': self.location.html_id(),
|
||||
'id': self.id,
|
||||
@@ -265,6 +319,7 @@ class CapaModule(CapaFields, XModule):
|
||||
def check_button_name(self):
|
||||
"""
|
||||
Determine the name for the "check" button.
|
||||
|
||||
Usually it is just "Check", but if this is the student's
|
||||
final attempt, change the name to "Final Check"
|
||||
"""
|
||||
@@ -350,27 +405,26 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
def handle_problem_html_error(self, err):
|
||||
"""
|
||||
Change our problem to a dummy problem containing
|
||||
a warning message to display to users.
|
||||
Create a dummy problem to represent any errors.
|
||||
|
||||
Returns the HTML to show to users
|
||||
Change our problem to a dummy problem containing a warning message to
|
||||
display to users. Returns the HTML to show to users
|
||||
|
||||
*err* is the Exception encountered while rendering the problem HTML.
|
||||
`err` is the Exception encountered while rendering the problem HTML.
|
||||
"""
|
||||
log.exception(err)
|
||||
log.exception(err.message)
|
||||
|
||||
# TODO (vshnayder): another switch on DEBUG.
|
||||
if self.system.DEBUG:
|
||||
msg = (
|
||||
'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
'Failed to generate HTML for problem %s</font>' %
|
||||
(self.location.url()))
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
u'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
u'Failed to generate HTML for problem {url}</font>'.format(
|
||||
url=cgi.escape(self.location.url()))
|
||||
)
|
||||
msg += u'<p>Error:</p><p><pre>{msg}</pre></p>'.format(msg=cgi.escape(err.message))
|
||||
msg += u'<p><pre>{tb}</pre></p>'.format(tb=cgi.escape(traceback.format_exc()))
|
||||
html = msg
|
||||
|
||||
# We're in non-debug mode, and possibly even in production. We want
|
||||
# to avoid bricking of problem as much as possible
|
||||
else:
|
||||
# We're in non-debug mode, and possibly even in production. We want
|
||||
# to avoid bricking of problem as much as possible
|
||||
@@ -416,8 +470,12 @@ class CapaModule(CapaFields, XModule):
|
||||
return html
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
'''Return html for the problem. Adds check, reset, save buttons
|
||||
as necessary based on the problem config and state.'''
|
||||
"""
|
||||
Return html for the problem.
|
||||
|
||||
Adds check, reset, save buttons as necessary based on the problem config
|
||||
and state.
|
||||
"""
|
||||
|
||||
try:
|
||||
html = self.lcp.get_html()
|
||||
@@ -454,22 +512,24 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
html = self.system.render_template('problem.html', context)
|
||||
if encapsulate:
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
html = u'<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url
|
||||
) + html + "</div>"
|
||||
|
||||
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
|
||||
return self.system.replace_urls(html)
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
"""
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
"get" is request.POST.
|
||||
|
||||
`get` is request.POST.
|
||||
|
||||
Returns a json dictionary:
|
||||
{ 'progress_changed' : True/False,
|
||||
'progress' : 'none'/'in_progress'/'done',
|
||||
<other request-specific values here > }
|
||||
'''
|
||||
"""
|
||||
handlers = {
|
||||
'problem_get': self.get_problem,
|
||||
'problem_check': self.check_problem,
|
||||
@@ -508,7 +568,9 @@ class CapaModule(CapaFields, XModule):
|
||||
datetime.datetime.now(UTC()) > self.close_date)
|
||||
|
||||
def closed(self):
|
||||
''' Is the student still allowed to submit answers? '''
|
||||
"""
|
||||
Is the student still allowed to submit answers?
|
||||
"""
|
||||
if self.max_attempts is not None and self.attempts >= self.max_attempts:
|
||||
return True
|
||||
if self.is_past_due():
|
||||
@@ -527,18 +589,24 @@ class CapaModule(CapaFields, XModule):
|
||||
return self.lcp.done
|
||||
|
||||
def is_attempted(self):
|
||||
"""Used by conditional module"""
|
||||
"""
|
||||
Has the problem been attempted?
|
||||
|
||||
used by conditional module
|
||||
"""
|
||||
return self.attempts > 0
|
||||
|
||||
def is_correct(self):
|
||||
"""True if full points"""
|
||||
"""
|
||||
True iff full points
|
||||
"""
|
||||
d = self.get_score()
|
||||
return d['score'] == d['total']
|
||||
|
||||
def answer_available(self):
|
||||
'''
|
||||
"""
|
||||
Is the user allowed to see an answer?
|
||||
'''
|
||||
"""
|
||||
if self.showanswer == '':
|
||||
return False
|
||||
elif self.showanswer == "never":
|
||||
@@ -570,7 +638,7 @@ class CapaModule(CapaFields, XModule):
|
||||
Delivers grading response (e.g. from asynchronous code checking) to
|
||||
the capa problem, so its score can be updated
|
||||
|
||||
'get' must have a field 'response' which is a string that contains the
|
||||
`get` must have a field `response` which is a string that contains the
|
||||
grader's response
|
||||
|
||||
No ajax return is needed. Return empty dict.
|
||||
@@ -584,7 +652,7 @@ class CapaModule(CapaFields, XModule):
|
||||
return dict() # No AJAX return is needed
|
||||
|
||||
def handle_ungraded_response(self, get):
|
||||
'''
|
||||
"""
|
||||
Delivers a response from the XQueue to the capa problem
|
||||
|
||||
The score of the problem will not be updated
|
||||
@@ -597,7 +665,7 @@ class CapaModule(CapaFields, XModule):
|
||||
empty dictionary
|
||||
|
||||
No ajax return is needed, so an empty dict is returned
|
||||
'''
|
||||
"""
|
||||
queuekey = get['queuekey']
|
||||
score_msg = get['xqueue_body']
|
||||
# pass along the xqueue message to the problem
|
||||
@@ -606,25 +674,25 @@ class CapaModule(CapaFields, XModule):
|
||||
return dict()
|
||||
|
||||
def handle_input_ajax(self, get):
|
||||
'''
|
||||
"""
|
||||
Handle ajax calls meant for a particular input in the 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)
|
||||
# save any state changes that may occur
|
||||
self.set_state_from_lcp()
|
||||
return response
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
"""
|
||||
For the "show answer" button.
|
||||
|
||||
Returns the answers: {'answers' : answers}
|
||||
'''
|
||||
"""
|
||||
event_info = dict()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
self.system.track_function('showanswer', event_info)
|
||||
@@ -641,7 +709,8 @@ class CapaModule(CapaFields, XModule):
|
||||
try:
|
||||
new_answer = {answer_id: self.system.replace_urls(answers[answer_id])}
|
||||
except TypeError:
|
||||
log.debug('Unable to perform URL substitution on answers[%s]: %s' % (answer_id, answers[answer_id]))
|
||||
log.debug(u'Unable to perform URL substitution on answers[%s]: %s',
|
||||
answer_id, answers[answer_id])
|
||||
new_answer = {answer_id: answers[answer_id]}
|
||||
new_answers.update(new_answer)
|
||||
|
||||
@@ -649,40 +718,44 @@ class CapaModule(CapaFields, XModule):
|
||||
|
||||
# Figure out if we should move these to capa_problem?
|
||||
def get_problem(self, get):
|
||||
''' Return results of get_problem_html, as a simple dict for json-ing.
|
||||
"""
|
||||
Return results of get_problem_html, as a simple dict for json-ing.
|
||||
|
||||
{ 'html': <the-html> }
|
||||
|
||||
Used if we want to reconfirm we have the right thing e.g. after
|
||||
several AJAX calls.
|
||||
'''
|
||||
Used if we want to reconfirm we have the right thing e.g. after
|
||||
several AJAX calls.
|
||||
"""
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
@staticmethod
|
||||
def make_dict_of_responses(get):
|
||||
'''Make dictionary of student responses (aka "answers")
|
||||
get is POST dictionary (Django QueryDict).
|
||||
"""
|
||||
Make dictionary of student responses (aka "answers")
|
||||
|
||||
The *get* dict has keys of the form 'x_y', which are mapped
|
||||
`get` is POST dictionary (Django QueryDict).
|
||||
|
||||
The `get` dict has keys of the form 'x_y', which are mapped
|
||||
to key 'y' in the returned dict. For example,
|
||||
'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
|
||||
|
||||
Some inputs always expect a list in the returned dict
|
||||
(e.g. checkbox inputs). The convention is that
|
||||
keys in the *get* dict that end with '[]' will always
|
||||
keys in the `get` dict that end with '[]' will always
|
||||
have list values in the returned dict.
|
||||
For example, if the *get* dict contains {'input_1[]': 'test' }
|
||||
For example, if the `get` dict contains {'input_1[]': 'test' }
|
||||
then the output dict would contain {'1': ['test'] }
|
||||
(the value is a list).
|
||||
|
||||
Raises an exception if:
|
||||
|
||||
A key in the *get* dictionary does not contain >= 1 underscores
|
||||
(e.g. "input" is invalid; "input_1" is valid)
|
||||
-A key in the `get` dictionary does not contain at least one underscore
|
||||
(e.g. "input" is invalid, but "input_1" is valid)
|
||||
|
||||
Two keys end up with the same name in the returned dict.
|
||||
(e.g. 'input_1' and 'input_1[]', which both get mapped
|
||||
to 'input_1' in the returned dict)
|
||||
'''
|
||||
-Two keys end up with the same name in the returned dict.
|
||||
(e.g. 'input_1' and 'input_1[]', which both get mapped to 'input_1'
|
||||
in the returned dict)
|
||||
"""
|
||||
answers = dict()
|
||||
|
||||
for key in get:
|
||||
@@ -693,7 +766,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# will return (key, '', '')
|
||||
# We detect this and raise an error
|
||||
if not name:
|
||||
raise ValueError("%s must contain at least one underscore" % str(key))
|
||||
raise ValueError(u"{key} must contain at least one underscore".format(key=key))
|
||||
|
||||
else:
|
||||
# This allows for answers which require more than one value for
|
||||
@@ -711,7 +784,7 @@ class CapaModule(CapaFields, XModule):
|
||||
# If the name already exists, then we don't want
|
||||
# to override it. Raise an error instead
|
||||
if name in answers:
|
||||
raise ValueError("Key %s already exists in answers dict" % str(name))
|
||||
raise ValueError(u"Key {name} already exists in answers dict".format(name=name))
|
||||
else:
|
||||
answers[name] = val
|
||||
|
||||
@@ -729,12 +802,13 @@ class CapaModule(CapaFields, XModule):
|
||||
})
|
||||
|
||||
def check_problem(self, get):
|
||||
''' Checks whether answers to a problem are correct, and
|
||||
returns a map of correct/incorrect answers:
|
||||
"""
|
||||
Checks whether answers to a problem are correct
|
||||
|
||||
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
|
||||
'contents' : html}
|
||||
'''
|
||||
Returns a map of correct/incorrect answers:
|
||||
{'success' : 'correct' | 'incorrect' | AJAX alert msg string,
|
||||
'contents' : html}
|
||||
"""
|
||||
event_info = dict()
|
||||
event_info['state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
@@ -759,7 +833,8 @@ class CapaModule(CapaFields, XModule):
|
||||
prev_submit_time = self.lcp.get_recentmost_queuetime()
|
||||
waittime_between_requests = self.system.xqueue['waittime']
|
||||
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
|
||||
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
|
||||
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
|
||||
|
||||
try:
|
||||
@@ -776,19 +851,19 @@ class CapaModule(CapaFields, XModule):
|
||||
# the full exception, including traceback,
|
||||
# in the response
|
||||
if self.system.user_is_staff:
|
||||
msg = "Staff debug info: %s" % traceback.format_exc()
|
||||
msg = u"Staff debug info: {tb}".format(tb=cgi.escape(traceback.format_exc()))
|
||||
|
||||
# Otherwise, display just an error message,
|
||||
# without a stack trace
|
||||
else:
|
||||
msg = "Error: %s" % str(inst.message)
|
||||
msg = u"Error: {msg}".format(msg=inst.message)
|
||||
|
||||
return {'success': msg}
|
||||
|
||||
except Exception as err:
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
msg = u"Error checking problem: {}".format(err.message)
|
||||
msg += u'\nTraceback:\n{}'.format(traceback.format_exc())
|
||||
return {'success': msg}
|
||||
raise
|
||||
|
||||
@@ -937,16 +1012,17 @@ class CapaModule(CapaFields, XModule):
|
||||
'msg': msg}
|
||||
|
||||
def reset_problem(self, get):
|
||||
''' Changes problem state to unfinished -- removes student answers,
|
||||
and causes problem to rerender itself.
|
||||
"""
|
||||
Changes problem state to unfinished -- removes student answers,
|
||||
and causes problem to rerender itself.
|
||||
|
||||
Returns a dictionary of the form:
|
||||
{'success': True/False,
|
||||
'html': Problem HTML string }
|
||||
Returns a dictionary of the form:
|
||||
{'success': True/False,
|
||||
'html': Problem HTML string }
|
||||
|
||||
If an error occurs, the dictionary will also have an
|
||||
'error' key containing an error message.
|
||||
'''
|
||||
If an error occurs, the dictionary will also have an
|
||||
`error` key containing an error message.
|
||||
"""
|
||||
event_info = dict()
|
||||
event_info['old_state'] = self.lcp.get_state()
|
||||
event_info['problem_id'] = self.location.url()
|
||||
@@ -993,7 +1069,8 @@ class CapaDescriptor(CapaFields, RawDescriptor):
|
||||
mako_template = "widgets/problem-edit.html"
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
|
||||
js_module_name = "MarkdownEditingDescriptor"
|
||||
css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'), resource_string(__name__, 'css/problem/edit.scss')]}
|
||||
css = {'scss': [resource_string(__name__, 'css/editor/edit.scss'),
|
||||
resource_string(__name__, 'css/problem/edit.scss')]}
|
||||
|
||||
# Capa modules have some additional metadata:
|
||||
# TODO (vshnayder): do problems have any other metadata? Do they
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Tests of the Capa XModule"""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tests of the Capa XModule
|
||||
"""
|
||||
#pylint: disable=C0111
|
||||
#pylint: disable=R0904
|
||||
#pylint: disable=C0103
|
||||
@@ -8,11 +11,12 @@ import datetime
|
||||
from mock import Mock, patch
|
||||
import unittest
|
||||
import random
|
||||
import json
|
||||
|
||||
import xmodule
|
||||
from capa.responsetypes import StudentInputError, \
|
||||
LoncapaProblemError, ResponseError
|
||||
from xmodule.capa_module import CapaModule
|
||||
from capa.responsetypes import (StudentInputError, LoncapaProblemError,
|
||||
ResponseError)
|
||||
from xmodule.capa_module import CapaModule, ComplexEncoder
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from django.http import QueryDict
|
||||
@@ -47,12 +51,16 @@ class CapaFactory(object):
|
||||
|
||||
@staticmethod
|
||||
def input_key():
|
||||
""" Return the input key to use when passing GET parameters """
|
||||
"""
|
||||
Return the input key to use when passing GET parameters
|
||||
"""
|
||||
return ("input_" + CapaFactory.answer_key())
|
||||
|
||||
@staticmethod
|
||||
def answer_key():
|
||||
""" Return the key stored in the capa problem answer dict """
|
||||
"""
|
||||
Return the key stored in the capa problem answer dict
|
||||
"""
|
||||
return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
|
||||
'SampleProblem%d' % CapaFactory.num]) +
|
||||
"_2_1")
|
||||
@@ -361,7 +369,9 @@ class CapaModuleTest(unittest.TestCase):
|
||||
result = CapaModule.make_dict_of_responses(invalid_get_dict)
|
||||
|
||||
def _querydict_from_dict(self, param_dict):
|
||||
""" Create a Django QueryDict from a Python dictionary """
|
||||
"""
|
||||
Create a Django QueryDict from a Python dictionary
|
||||
"""
|
||||
|
||||
# QueryDict objects are immutable by default, so we make
|
||||
# a copy that we can update.
|
||||
@@ -496,9 +506,10 @@ class CapaModuleTest(unittest.TestCase):
|
||||
def test_check_problem_error(self):
|
||||
|
||||
# Try each exception that capa_module should handle
|
||||
for exception_class in [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]:
|
||||
exception_classes = [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]
|
||||
for exception_class in exception_classes:
|
||||
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1)
|
||||
@@ -520,6 +531,60 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_other_errors(self):
|
||||
"""
|
||||
Test that errors other than the expected kinds give an appropriate message.
|
||||
|
||||
See also `test_check_problem_error` for the "expected kinds" or errors.
|
||||
"""
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Ensure that the user is NOT staff
|
||||
module.system.user_is_staff = False
|
||||
|
||||
# Ensure that DEBUG is on
|
||||
module.system.DEBUG = True
|
||||
|
||||
# Simulate answering a problem that raises the exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
error_msg = u"Superterrible error happened: ☠"
|
||||
mock_grade.side_effect = Exception(error_msg)
|
||||
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
self.assertTrue(error_msg in result['success'])
|
||||
|
||||
def test_check_problem_error_nonascii(self):
|
||||
|
||||
# Try each exception that capa_module should handle
|
||||
exception_classes = [StudentInputError,
|
||||
LoncapaProblemError,
|
||||
ResponseError]
|
||||
for exception_class in exception_classes:
|
||||
|
||||
# Create the module
|
||||
module = CapaFactory.create(attempts=1)
|
||||
|
||||
# Ensure that the user is NOT staff
|
||||
module.system.user_is_staff = False
|
||||
|
||||
# Simulate answering a problem that raises the exception
|
||||
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
|
||||
mock_grade.side_effect = exception_class(u"ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ")
|
||||
|
||||
get_request_dict = {CapaFactory.input_key(): '3.14'}
|
||||
result = module.check_problem(get_request_dict)
|
||||
|
||||
# Expect an AJAX alert message in 'success'
|
||||
expected_msg = u'Error: ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ'
|
||||
self.assertEqual(expected_msg, result['success'])
|
||||
|
||||
# Expect that the number of attempts is NOT incremented
|
||||
self.assertEqual(module.attempts, 1)
|
||||
|
||||
def test_check_problem_error_with_staff_user(self):
|
||||
|
||||
# Try each exception that capa module should handle
|
||||
@@ -1021,6 +1086,33 @@ class CapaModuleTest(unittest.TestCase):
|
||||
# Expect that the module has created a new dummy problem with the error
|
||||
self.assertNotEqual(original_problem, module.lcp)
|
||||
|
||||
def test_get_problem_html_error_w_debug(self):
|
||||
"""
|
||||
Test the html response when an error occurs with DEBUG on
|
||||
"""
|
||||
module = CapaFactory.create()
|
||||
|
||||
# Simulate throwing an exception when the capa problem
|
||||
# is asked to render itself as HTML
|
||||
error_msg = u"Superterrible error happened: ☠"
|
||||
module.lcp.get_html = Mock(side_effect=Exception(error_msg))
|
||||
|
||||
# Stub out the get_test_system rendering function
|
||||
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
|
||||
# Make sure DEBUG is on
|
||||
module.system.DEBUG = True
|
||||
|
||||
# Try to render the module with DEBUG turned on
|
||||
html = module.get_problem_html()
|
||||
|
||||
self.assertTrue(html is not None)
|
||||
|
||||
# Check the rendering context
|
||||
render_args, _ = module.system.render_template.call_args
|
||||
context = render_args[1]
|
||||
self.assertTrue(error_msg in context['problem']['html'])
|
||||
|
||||
def test_random_seed_no_change(self):
|
||||
|
||||
# Run the test for each possible rerandomize value
|
||||
@@ -1126,3 +1218,28 @@ class CapaModuleTest(unittest.TestCase):
|
||||
for i in range(200):
|
||||
module = CapaFactory.create(rerandomize=rerandomize)
|
||||
assert 0 <= module.seed < 1000
|
||||
|
||||
@patch('xmodule.capa_module.log')
|
||||
@patch('xmodule.capa_module.Progress')
|
||||
def test_get_progress_error(self, mock_progress, mock_log):
|
||||
"""
|
||||
Check that an exception given in `Progress` produces a `log.exception` call.
|
||||
"""
|
||||
error_types = [TypeError, ValueError]
|
||||
for error_type in error_types:
|
||||
mock_progress.side_effect = error_type
|
||||
module = CapaFactory.create()
|
||||
self.assertIsNone(module.get_progress())
|
||||
mock_log.exception.assert_called_once_with('Got bad progress')
|
||||
mock_log.reset_mock()
|
||||
|
||||
|
||||
class ComplexEncoderTest(unittest.TestCase):
|
||||
def test_default(self):
|
||||
"""
|
||||
Check that complex numbers can be encoded into JSON.
|
||||
"""
|
||||
complex_num = 1 - 1j
|
||||
expected_str = '1-1*j'
|
||||
json_str = json.dumps(complex_num, cls=ComplexEncoder)
|
||||
self.assertEqual(expected_str, json_str[1:-1]) # ignore quotes
|
||||
|
||||
Reference in New Issue
Block a user