Commit 80bc2c1e by Will Daly

Show a friendlier error message when the submission API reports a request error

Show a more specific error message for answer too long errors
parent c80d9141
...@@ -26,14 +26,15 @@ class SubmissionMixin(object): ...@@ -26,14 +26,15 @@ class SubmissionMixin(object):
""" """
submit_errors = { SUBMIT_ERRORS = {
# Reported to user sometimes, and useful in tests # Reported to user sometimes, and useful in tests
'ENODATA': _(u'API returned an empty response.'), 'ENODATA': _(u'An unexpected error occurred.'),
'EBADFORM': _(u'API Submission Request Error.'), 'EBADFORM': _(u'An unexpected error occurred.'),
'EUNKNOWN': _(u'API returned unclassified exception.'), 'EUNKNOWN': _(u'An unexpected error occurred.'),
'ENOMULTI': _(u'Multiple submissions are not allowed.'), 'ENOMULTI': _(u'Multiple submissions are not allowed.'),
'ENOPREVIEW': _(u'To submit a response, view this component in Preview or Live mode.'), 'ENOPREVIEW': _(u'To submit a response, view this component in Preview or Live mode.'),
'EBADARGS': _(u'"submission" required to submit answer.') 'EBADARGS': _(u'An unexpected error occurred.'),
'EANSWERLENGTH': _(u'This response exceeds the size limit.')
} }
@XBlock.json_handler @XBlock.json_handler
...@@ -57,7 +58,7 @@ class SubmissionMixin(object): ...@@ -57,7 +58,7 @@ class SubmissionMixin(object):
""" """
if 'submission' not in data: if 'submission' not in data:
return False, 'EBADARGS', self.submit_errors['EBADARGS'] return False, 'EBADARGS', self.SUBMIT_ERRORS['EBADARGS']
status = False status = False
status_text = None status_text = None
...@@ -67,7 +68,7 @@ class SubmissionMixin(object): ...@@ -67,7 +68,7 @@ class SubmissionMixin(object):
# Short-circuit if no user is defined (as in Studio Preview mode) # Short-circuit if no user is defined (as in Studio Preview mode)
# Since students can't submit, they will never be able to progress in the workflow # Since students can't submit, they will never be able to progress in the workflow
if self.in_studio_preview: if self.in_studio_preview:
return False, 'ENOPREVIEW', self.submit_errors['ENOPREVIEW'] return False, 'ENOPREVIEW', self.SUBMIT_ERRORS['ENOPREVIEW']
workflow = self.get_workflow_info() workflow = self.get_workflow_info()
...@@ -80,10 +81,35 @@ class SubmissionMixin(object): ...@@ -80,10 +81,35 @@ class SubmissionMixin(object):
student_sub student_sub
) )
except api.SubmissionRequestError as err: except api.SubmissionRequestError as err:
status_tag = 'EBADFORM'
status_text = unicode(err.field_errors) # Handle the case of an answer that's too long as a special case,
# so we can display a more specific error message.
# Although we limit the number of characters the user can
# enter on the client side, the submissions API uses the JSON-serialized
# submission to calculate length. If each character submitted
# by the user takes more than 1 byte to encode (for example, double-escaped
# newline characters or non-ASCII unicode), then the user might
# exceed the limits set by the submissions API. In that case,
# we display an error message indicating that the answer is too long.
answer_too_long = any(
"maximum answer size exceeded" in answer_err.lower()
for answer_err in err.field_errors.get('answer', [])
)
if answer_too_long:
status_tag = 'EANSWERLENGTH'
else:
msg = (
u"The submissions API reported an invalid request error "
u"when submitting a response for the user: {student_item}"
).format(student_item=student_item_dict)
logger.exception(msg)
status_tag = 'EBADFORM'
except (api.SubmissionError, AssessmentWorkflowError): except (api.SubmissionError, AssessmentWorkflowError):
logger.exception("This response was not submitted.") msg = (
u"An unknown error occurred while submitting "
u"a response for the user: {student_item}"
).format(student_item=student_item_dict)
logger.exception(msg)
status_tag = 'EUNKNOWN' status_tag = 'EUNKNOWN'
else: else:
status = True status = True
...@@ -91,7 +117,7 @@ class SubmissionMixin(object): ...@@ -91,7 +117,7 @@ class SubmissionMixin(object):
status_text = submission.get('attempt_number') status_text = submission.get('attempt_number')
# relies on success being orthogonal to errors # relies on success being orthogonal to errors
status_text = status_text if status_text else self.submit_errors[status_tag] status_text = status_text if status_text else self.SUBMIT_ERRORS[status_tag]
return status, status_tag, status_text return status, status_tag, status_text
@XBlock.json_handler @XBlock.json_handler
......
...@@ -9,7 +9,6 @@ import pytz ...@@ -9,7 +9,6 @@ import pytz
from mock import patch, Mock from mock import patch, Mock
from submissions import api as sub_api from submissions import api as sub_api
from submissions.api import SubmissionRequestError, SubmissionInternalError from submissions.api import SubmissionRequestError, SubmissionInternalError
from openassessment.xblock.submission_mixin import SubmissionMixin
from .base import XBlockHandlerTestCase, scenario from .base import XBlockHandlerTestCase, scenario
...@@ -23,6 +22,17 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -23,6 +22,17 @@ class SubmissionTest(XBlockHandlerTestCase):
self.assertTrue(resp[0]) self.assertTrue(resp[0])
@scenario('data/basic_scenario.xml', user_id='Bob') @scenario('data/basic_scenario.xml', user_id='Bob')
def test_submit_answer_too_long(self, xblock):
# Maximum answer length is 100K, once the answer has been JSON-encoded
long_submission = json.dumps({
'submission': 'longcat is long ' * 100000
})
resp = self.request(xblock, 'submit', long_submission, response_format='json')
self.assertFalse(resp[0])
self.assertEqual(resp[1], "EANSWERLENGTH")
self.assertEqual(resp[2], xblock.SUBMIT_ERRORS["EANSWERLENGTH"])
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_submission_multisubmit_failure(self, xblock): def test_submission_multisubmit_failure(self, xblock):
# We don't care about return value of first one # We don't care about return value of first one
self.request(xblock, 'submit', self.SUBMISSION, response_format='json') self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
...@@ -31,7 +41,7 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -31,7 +41,7 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json') resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0]) self.assertFalse(resp[0])
self.assertEqual(resp[1], "ENOMULTI") self.assertEqual(resp[1], "ENOMULTI")
self.assertEqual(resp[2], xblock.submit_errors["ENOMULTI"]) self.assertEqual(resp[2], xblock.SUBMIT_ERRORS["ENOMULTI"])
@scenario('data/basic_scenario.xml', user_id='Bob') @scenario('data/basic_scenario.xml', user_id='Bob')
@patch.object(sub_api, 'create_submission') @patch.object(sub_api, 'create_submission')
...@@ -40,15 +50,16 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -40,15 +50,16 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json') resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0]) self.assertFalse(resp[0])
self.assertEqual(resp[1], "EUNKNOWN") self.assertEqual(resp[1], "EUNKNOWN")
self.assertEqual(resp[2], SubmissionMixin().submit_errors["EUNKNOWN"]) self.assertEqual(resp[2], xblock.SUBMIT_ERRORS["EUNKNOWN"])
@scenario('data/basic_scenario.xml', user_id='Bob') @scenario('data/basic_scenario.xml', user_id='Bob')
@patch.object(sub_api, 'create_submission') @patch.object(sub_api, 'create_submission')
def test_submission_API_failure(self, xblock, mock_submit): def test_submission_API_failure(self, xblock, mock_submit):
mock_submit.side_effect = SubmissionRequestError("Cat on fire.") mock_submit.side_effect = SubmissionRequestError(msg="Cat on fire.")
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json') resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0]) self.assertFalse(resp[0])
self.assertEqual(resp[1], "EBADFORM") self.assertEqual(resp[1], "EBADFORM")
self.assertEqual(resp[2], xblock.SUBMIT_ERRORS["EBADFORM"])
# In Studio preview mode, the runtime sets the user ID to None # In Studio preview mode, the runtime sets the user ID to None
@scenario('data/basic_scenario.xml', user_id=None) @scenario('data/basic_scenario.xml', user_id=None)
...@@ -65,7 +76,7 @@ class SubmissionTest(XBlockHandlerTestCase): ...@@ -65,7 +76,7 @@ class SubmissionTest(XBlockHandlerTestCase):
resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json') resp = self.request(xblock, 'submit', self.SUBMISSION, response_format='json')
self.assertFalse(resp[0]) self.assertFalse(resp[0])
self.assertEqual(resp[1], "ENOPREVIEW") self.assertEqual(resp[1], "ENOPREVIEW")
self.assertEqual(resp[2], "To submit a response, view this component in Preview or Live mode.") self.assertEqual(resp[2], xblock.SUBMIT_ERRORS["ENOPREVIEW"])
@scenario('data/over_grade_scenario.xml', user_id='Alice') @scenario('data/over_grade_scenario.xml', user_id='Alice')
def test_closed_submissions(self, xblock): def test_closed_submissions(self, xblock):
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
git+https://github.com/edx/XBlock.git@fc5fea25c973ec66d8db63cf69a817ce624f5ef5#egg=XBlock git+https://github.com/edx/XBlock.git@fc5fea25c973ec66d8db63cf69a817ce624f5ef5#egg=XBlock
git+https://github.com/edx/xblock-sdk.git@643900aadcb18aaeb7fe67271ca9dbf36e463ee6#egg=xblock-sdk git+https://github.com/edx/xblock-sdk.git@643900aadcb18aaeb7fe67271ca9dbf36e463ee6#egg=xblock-sdk
edx-submissions==0.0.3 edx-submissions==0.0.5
# Third Party Requirements # Third Party Requirements
boto==2.13.3 boto==2.13.3
......
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