Commit ac86687f by Will Daly

Added exception handling that solves SchematicResponse exceptions

causing a 500 error.

When XModule raises a ProcessingError during an AJAX request,
this module_render now returns a 404 to further reduce number of 500
responses.
parent 6edee96c
...@@ -53,12 +53,17 @@ class LoncapaProblemError(Exception): ...@@ -53,12 +53,17 @@ class LoncapaProblemError(Exception):
class ResponseError(Exception): class ResponseError(Exception):
''' '''
Error for failure in processing a response Error for failure in processing a response, including
exceptions that occur when executing a custom script.
''' '''
pass pass
class StudentInputError(Exception): class StudentInputError(Exception):
'''
Error for an invalid student input.
For example, submitting a string when the problem expects a number
'''
pass pass
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1151,7 +1156,7 @@ def sympy_check2(): ...@@ -1151,7 +1156,7 @@ def sympy_check2():
# Raise an exception # Raise an exception
else: else:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise LoncapaProblemError( raise ResponseError(
"CustomResponse: check function returned an invalid dict") "CustomResponse: check function returned an invalid dict")
# The check function can return a boolean value, # The check function can return a boolean value,
...@@ -1226,7 +1231,7 @@ def sympy_check2(): ...@@ -1226,7 +1231,7 @@ def sympy_check2():
Handle an exception raised during the execution of Handle an exception raised during the execution of
custom Python code. custom Python code.
Raises a StudentInputError Raises a ResponseError
''' '''
# Log the error if we are debugging # Log the error if we are debugging
...@@ -1236,7 +1241,7 @@ def sympy_check2(): ...@@ -1236,7 +1241,7 @@ def sympy_check2():
# Notify student with a student input error # Notify student with a student input error
_, _, traceback_obj = sys.exc_info() _, _, traceback_obj = sys.exc_info()
raise StudentInputError, StudentInputError(err.message), traceback_obj raise ResponseError, ResponseError(err.message), traceback_obj
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -1912,7 +1917,14 @@ class SchematicResponse(LoncapaResponse): ...@@ -1912,7 +1917,14 @@ class SchematicResponse(LoncapaResponse):
submission = [json.loads(student_answers[ submission = [json.loads(student_answers[
k]) for k in sorted(self.answer_ids)] k]) for k in sorted(self.answer_ids)]
self.context.update({'submission': submission}) self.context.update({'submission': submission})
try:
exec self.code in global_context, self.context exec self.code in global_context, self.context
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ResponseError, ResponseError(err.message), traceback_obj
cmap = CorrectMap() cmap = CorrectMap()
cmap.set_dict(dict(zip(sorted( cmap.set_dict(dict(zip(sorted(
self.answer_ids), self.context['correct']))) self.answer_ids), self.context['correct'])))
......
...@@ -12,12 +12,13 @@ from lxml import etree ...@@ -12,12 +12,13 @@ from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError, \
ResponseError, LoncapaProblemError
from capa.util import convert_files_to_filenames from capa.util import convert_files_to_filenames
from .progress import Progress from .progress import Progress
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float from xblock.core import Integer, Scope, BlockScope, ModelType, String, Boolean, Object, Float
from .fields import Timedelta from .fields import Timedelta
...@@ -454,7 +455,14 @@ class CapaModule(CapaFields, XModule): ...@@ -454,7 +455,14 @@ class CapaModule(CapaFields, XModule):
return 'Error' return 'Error'
before = self.get_progress() before = self.get_progress()
try:
d = handlers[dispatch](get) d = handlers[dispatch](get)
except Exception as err:
_, _, traceback_obj = sys.exc_info()
raise ProcessingError, ProcessingError(err.message), traceback_obj
after = self.get_progress() after = self.get_progress()
d.update({ d.update({
'progress_changed': after != before, 'progress_changed': after != before,
...@@ -726,7 +734,7 @@ class CapaModule(CapaFields, XModule): ...@@ -726,7 +734,7 @@ class CapaModule(CapaFields, XModule):
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
self.set_state_from_lcp() self.set_state_from_lcp()
except StudentInputError as inst: except (StudentInputError, ResponseError, LoncapaProblemError) as inst:
log.exception("StudentInputError in capa_module:problem_check") log.exception("StudentInputError in capa_module:problem_check")
# If the user is a staff member, include # If the user is a staff member, include
......
class InvalidDefinitionError(Exception): class InvalidDefinitionError(Exception):
pass pass
class NotFoundError(Exception): class NotFoundError(Exception):
pass pass
class ProcessingError(Exception):
'''
An error occurred while processing a request to the XModule.
For example: if an exception occurs while checking a capa problem.
'''
pass
...@@ -7,6 +7,8 @@ import random ...@@ -7,6 +7,8 @@ import random
import xmodule import xmodule
import capa import capa
from capa.responsetypes import StudentInputError, \
LoncapaProblemError, ResponseError
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
...@@ -502,15 +504,22 @@ class CapaModuleTest(unittest.TestCase): ...@@ -502,15 +504,22 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(module.attempts, 1) self.assertEqual(module.attempts, 1)
def test_check_problem_student_input_error(self): def test_check_problem_error(self):
# Try each exception that capa_module should handle
for exception_class in [StudentInputError,
LoncapaProblemError,
ResponseError]:
# Create the module
module = CapaFactory.create(attempts=1) module = CapaFactory.create(attempts=1)
# Ensure that the user is NOT staff # Ensure that the user is NOT staff
module.system.user_is_staff = False module.system.user_is_staff = False
# Simulate a student input exception # Simulate answering a problem that raises the exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') mock_grade.side_effect = exception_class('test error')
get_request_dict = {CapaFactory.input_key(): '3.14'} get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
...@@ -522,15 +531,22 @@ class CapaModuleTest(unittest.TestCase): ...@@ -522,15 +531,22 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the number of attempts is NOT incremented # Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1) self.assertEqual(module.attempts, 1)
def test_check_problem_student_input_error_with_staff_user(self): def test_check_problem_error_with_staff_user(self):
# Try each exception that capa module should handle
for exception_class in [StudentInputError,
LoncapaProblemError,
ResponseError]:
# Create the module
module = CapaFactory.create(attempts=1) module = CapaFactory.create(attempts=1)
# Ensure that the user IS staff # Ensure that the user IS staff
module.system.user_is_staff = True module.system.user_is_staff = True
# Simulate a student input exception # Simulate answering a problem that raises an exception
with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade: with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
mock_grade.side_effect = capa.responsetypes.StudentInputError('test error') mock_grade.side_effect = exception_class('test error')
get_request_dict = {CapaFactory.input_key(): '3.14'} get_request_dict = {CapaFactory.input_key(): '3.14'}
result = module.check_problem(get_request_dict) result = module.check_problem(get_request_dict)
...@@ -541,6 +557,10 @@ class CapaModuleTest(unittest.TestCase): ...@@ -541,6 +557,10 @@ class CapaModuleTest(unittest.TestCase):
# We DO include traceback information for staff users # We DO include traceback information for staff users
self.assertTrue('Traceback' in result['success']) self.assertTrue('Traceback' in result['success'])
# Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1)
def test_reset_problem(self): def test_reset_problem(self):
module = CapaFactory.create(done=True) module = CapaFactory.create(done=True)
module.new_lcp = Mock(wraps=module.new_lcp) module.new_lcp = Mock(wraps=module.new_lcp)
......
...@@ -22,7 +22,7 @@ from .models import StudentModule ...@@ -22,7 +22,7 @@ from .models import StudentModule
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import ModuleSystem from xmodule.x_module import ModuleSystem
...@@ -443,9 +443,18 @@ def modx_dispatch(request, dispatch, location, course_id): ...@@ -443,9 +443,18 @@ def modx_dispatch(request, dispatch, location, course_id):
# Let the module handle the AJAX # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, p) ajax_return = instance.handle_ajax(dispatch, p)
# If we can't find the module, respond with a 404
except NotFoundError: except NotFoundError:
log.exception("Module indicating to user that request doesn't exist") log.exception("Module indicating to user that request doesn't exist")
raise Http404 raise Http404
# For XModule-specific errors, we respond with 404
except ProcessingError:
log.exception("Module encountered an error while prcessing AJAX call")
raise Http404
# If any other error occurred, re-raise it to trigger a 500 response
except: except:
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment