diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 1bb3e115b6..5cc27ce573 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -230,7 +230,6 @@ class LoncapaProblem(object): if hasattr(the_input, 'ungraded_response'): 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 @@ -238,7 +237,6 @@ class LoncapaProblem(object): ''' 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 @@ -256,7 +254,6 @@ class LoncapaProblem(object): return max(queuetimes) - def grade_answers(self, answers): ''' Grade student responses. Called by capa_module.check_problem. @@ -272,6 +269,31 @@ class LoncapaProblem(object): self.student_answers = convert_files_to_filenames(answers) return self._grade_answers(answers) + def supports_regrading(self): + """ + Checks that the current problem definition permits regrading. + + More precisely, it checks that there are no response types in + the current problem that are not fully supported (yet) for regrading. + + This includes responsetypes for which the student's answer + is not properly stored in state, i.e. file submissions. At present, + we have no way to know if an existing response was actually a real + answer or merely the filename of a file submitted as an answer. + + It turns out that because regrading is a background task, limiting + it to responsetypes that don't support file submissions also means + that the responsetypes are synchronous. This is convenient as it + permits regrading to be complete when the regrading call returns. + """ + # We check for synchronous grading and no file submissions by + # screening out all problems with a CodeResponse type. + for responder in self.responders.values(): + if 'filesubmission' in responder.allowed_inputfields: + return False + + return True + def regrade_existing_answers(self): ''' Regrade student responses. Called by capa_module.regrade_problem. @@ -298,14 +320,21 @@ class LoncapaProblem(object): # log.debug('Responders: %s' % self.responders) # Call each responsetype instance to do actual grading for responder in self.responders.values(): - # File objects are passed only if responsetype explicitly allows for file - # submissions + # File objects are passed only if responsetype explicitly allows + # for file submissions. But we have no way of knowing if + # student_answers contains a proper answer or the filename of + # an earlier submission, so for now skip these entirely. # TODO: figure out where to get file submissions when regrading. - if 'filesubmission' in responder.allowed_inputfields and answers is not None: + if 'filesubmission' in responder.allowed_inputfields and answers is None: + raise Exception("Cannot regrade problems with possible file submissions") + + # use 'answers' if it is provided, otherwise use the saved student_answers. + if answers is not None: results = responder.evaluate_answers(answers, oldcmap) else: results = responder.evaluate_answers(self.student_answers, oldcmap) newcmap.update(results) + self.correct_map = newcmap # log.debug('%s: in grade_answers, answers=%s, cmap=%s' % (self,answers,newcmap)) return newcmap diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index 07dfe5e0f7..306fb38d0e 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -822,14 +822,21 @@ class CapaModule(CapaFields, XModule): Returns a dict with one key: {'success' : 'correct' | 'incorrect' | AJAX alert msg string } - Raises NotFoundError if called on a problem that has not yet been answered - (since this is avoidable). Returns the error messages for exceptions - occurring while performing the regrading, rather than throwing them. + Raises NotFoundError if called on a problem that has not yet been + answered, or if it's a problem that cannot be regraded. + + Returns the error messages for exceptions occurring while performing + the regrading, rather than throwing them. """ event_info = dict() event_info['state'] = self.lcp.get_state() event_info['problem_id'] = self.location.url() + if not self.lcp.supports_regrading(): + event_info['failure'] = 'unsupported' + self.system.track_function('problem_regrade_fail', event_info) + raise NotFoundError('Problem does not support regrading') + if not self.done: event_info['failure'] = 'unanswered' self.system.track_function('problem_regrade_fail', event_info) diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py index 738f5a49f3..2a31b6478a 100644 --- a/common/lib/xmodule/xmodule/tests/test_capa_module.py +++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py @@ -641,6 +641,16 @@ class CapaModuleTest(unittest.TestCase): with self.assertRaises(xmodule.exceptions.NotFoundError): module.regrade_problem() + def test_regrade_problem_not_supported(self): + # Simulate that the problem is NOT done + module = CapaFactory.create(done=True) + + # Try to regrade the problem, and get exception + with patch('capa.capa_problem.LoncapaProblem.supports_regrading') as mock_supports_regrading: + mock_supports_regrading.return_value = False + with self.assertRaises(xmodule.exceptions.NotFoundError): + module.regrade_problem() + def test_regrade_problem_error(self): # Try each exception that capa_module should handle