diff --git a/lms/djangoapps/courseware/task_queue.py b/lms/djangoapps/courseware/task_queue.py index d846375c27..90cdd7f765 100644 --- a/lms/djangoapps/courseware/task_queue.py +++ b/lms/djangoapps/courseware/task_queue.py @@ -16,6 +16,10 @@ from xmodule.modulestore.django import modulestore log = logging.getLogger(__name__) +class AlreadyRunningError(Exception): + pass + + def get_running_course_tasks(course_id): """ Returns a query of CourseTaskLog objects of running tasks for a given course. @@ -85,7 +89,7 @@ def _reserve_task(course_id, task_name, task_args, requester, student=None): """ if _task_is_running(course_id, task_name, task_args, student): - raise Exception("requested task is already running") + raise AlreadyRunningError("requested task is already running") # Create log entry now, so that future requests won't tasklog_args = {'course_id': course_id, @@ -157,7 +161,7 @@ def _update_course_task_log(course_task_log_entry, task_result): total=returned_result['total'], action_name=returned_result['action_name']) output['message'] = message - log.info("task progress: {0}".format(message)) + log.info("task progress: %s", message) else: log.info("still making progress... ") output['task_progress'] = returned_result @@ -165,7 +169,7 @@ def _update_course_task_log(course_task_log_entry, task_result): elif result_state == 'SUCCESS': output['task_progress'] = returned_result course_task_log_entry.task_progress = json.dumps(returned_result) - log.info("task succeeded: {0}".format(returned_result)) + log.info("task succeeded: %s", returned_result) entry_needs_saving = True elif result_state == 'FAILURE': @@ -175,13 +179,23 @@ def _update_course_task_log(course_task_log_entry, task_result): entry_needs_saving = True task_progress = {'exception': type(exception).__name__, 'message': str(exception.message)} output['message'] = exception.message - log.warning("background task (%s) failed: %s %s".format(task_id, returned_result, traceback)) + log.warning("background task (%s) failed: %s %s", task_id, returned_result, traceback) if result_traceback is not None: output['task_traceback'] = result_traceback task_progress['traceback'] = result_traceback course_task_log_entry.task_progress = json.dumps(task_progress) output['task_progress'] = task_progress + elif result_state == 'REVOKED': + # on revocation, the result's result doesn't contain anything + entry_needs_saving = True + message = 'Task revoked before running' + output['message'] = message + log.warning("background task (%s) revoked.", task_id) + task_progress = {'message': message} + course_task_log_entry.task_progress = json.dumps(task_progress) + output['task_progress'] = task_progress + # always update the entry if the state has changed: if result_state != course_task_log_entry.task_state: course_task_log_entry.task_state = result_state @@ -308,7 +322,7 @@ def _check_arguments_for_regrading(course_id, problem_url): """ descriptor = modulestore().get_instance(course_id, problem_url) supports_regrade = False - if hasattr(descriptor,'module_class'): + if hasattr(descriptor, 'module_class'): module_class = descriptor.module_class if hasattr(module_class, 'regrade_problem'): supports_regrade = True diff --git a/lms/djangoapps/courseware/tests/factories.py b/lms/djangoapps/courseware/tests/factories.py index 26df68ca7e..023cb4ef06 100644 --- a/lms/djangoapps/courseware/tests/factories.py +++ b/lms/djangoapps/courseware/tests/factories.py @@ -10,6 +10,8 @@ from student.tests.factories import CourseEnrollmentAllowedFactory as StudentCou from student.tests.factories import RegistrationFactory as StudentRegistrationFactory from courseware.models import StudentModule, XModuleContentField, XModuleSettingsField from courseware.models import XModuleStudentInfoField, XModuleStudentPrefsField +from courseware.models import CourseTaskLog + from xmodule.modulestore import Location from pytz import UTC @@ -84,3 +86,16 @@ class StudentInfoFactory(DjangoModelFactory): field_name = 'existing_field' value = json.dumps('old_value') student = SubFactory(UserFactory) + + +class CourseTaskLogFactory(DjangoModelFactory): + FACTORY_FOR = CourseTaskLog + + task_name = 'regrade_problem' + course_id = "MITx/999/Robot_Super_Course" + student = SubFactory(UserFactory) + task_args = None + task_id = None + task_state = "QUEUED" + task_progress = None + requester = SubFactory(UserFactory) diff --git a/lms/djangoapps/courseware/tests/test_task_queue.py b/lms/djangoapps/courseware/tests/test_task_queue.py new file mode 100644 index 0000000000..3a20fb237d --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_task_queue.py @@ -0,0 +1,278 @@ +""" +Test for LMS courseware background task queue management +""" +import logging +import json +from mock import Mock, patch +from uuid import uuid4 + +from django.utils.datastructures import MultiValueDict +from django.test.testcases import TestCase + +from xmodule.modulestore.exceptions import ItemNotFoundError + +from courseware.tests.factories import UserFactory, CourseTaskLogFactory +from courseware.task_queue import (get_running_course_tasks, + course_task_log_status, + AlreadyRunningError, + submit_regrade_problem_for_all_students, + submit_regrade_problem_for_student, + submit_reset_problem_attempts_for_all_students, + submit_delete_problem_state_for_all_students) + + +log = logging.getLogger("mitx." + __name__) + + +TEST_FAILURE_MESSAGE = 'task failed horribly' + + +class TaskQueueTestCase(TestCase): + """ + Check that background tasks are properly queued and report status. + """ + student = None + instructor = None + problem_url = None + + def setUp(self): + self.student = UserFactory.create(username="student", email="student@edx.org") + self.instructor = UserFactory.create(username="instructor", email="student@edx.org") + self.problem_url = TaskQueueTestCase.problem_location("test_urlname") + + @staticmethod + def problem_location(problem_url_name): + """ + Create an internal location for a test problem. + """ + if "i4x:" in problem_url_name: + return problem_url_name + else: + return "i4x://{org}/{number}/problem/{problem_url_name}".format(org='edx', + number='1.23x', + problem_url_name=problem_url_name) + + def _create_entry(self, task_state="QUEUED", task_progress=None, student=None): + task_id = str(uuid4()) + progress_json = json.dumps(task_progress) + course_task_log = CourseTaskLogFactory.create(student=student, + requester=self.instructor, + task_args=self.problem_url, + task_id=task_id, + task_state=task_state, + task_progress=progress_json) + return course_task_log + + def _create_failure_entry(self): + # view task entry for task failure + progress = {'message': TEST_FAILURE_MESSAGE, + 'exception': 'RandomCauseError', + } + return self._create_entry(task_state="FAILURE", task_progress=progress) + + def _create_success_entry(self, student=None): + return self._create_progress_entry(student=None, task_state="SUCCESS") + + def _create_progress_entry(self, student=None, task_state="PROGRESS"): + # view task entry for task failure + progress = {'attempted': 3, + 'updated': 2, + 'total': 10, + 'action_name': 'regraded', + 'message': 'some random string that should summarize the other info', + } + return self._create_entry(task_state=task_state, task_progress=progress, student=student) + + def test_fetch_running_tasks(self): + # when fetching running tasks, we get all running tasks, and only running tasks + failure_task_ids = [(self._create_failure_entry()).task_id for _ in range(1, 4)] + entry = self._create_failure_entry() + failure_task_ids.append(entry.task_id) + course_id = entry.course_id # get course_id used by the factory + success_task_ids = [(self._create_success_entry()).task_id for _ in range(1, 5)] + progress_task_ids = [(self._create_progress_entry()).task_id for _ in range(1, 5)] + task_ids = [course_task_log.task_id for course_task_log in get_running_course_tasks(course_id)] + self.assertEquals(len(task_ids), len(progress_task_ids)) + for task_id in task_ids: + self.assertTrue(task_id in progress_task_ids) + self.assertFalse(task_id in success_task_ids) + self.assertFalse(task_id in failure_task_ids) + + def test_course_task_log_status_by_post(self): + # fetch status for existing tasks: by arg is tested elsewhere, + # so test by POST arg + course_task_log = self._create_failure_entry() + task_id = course_task_log.task_id + request = Mock() + request.POST = {} + request.POST['task_id'] = task_id + response = course_task_log_status(request) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + + def test_course_task_log_status_list_by_post(self): + # Fetch status for existing tasks: by arg is tested elsewhere, + # so test here by POST arg list, as if called from ajax. + # Note that ajax does something funny with the marshalling of + # list data, so the key value has "[]" appended to it. + task_ids = [(self._create_failure_entry()).task_id for _ in range(1, 5)] + request = Mock() + request.POST = MultiValueDict({'task_ids[]': task_ids}) + response = course_task_log_status(request) + output = json.loads(response.content) + for task_id in task_ids: + self.assertEquals(output[task_id]['task_id'], task_id) + + def test_initial_failure(self): + course_task_log = self._create_failure_entry() + task_id = course_task_log.task_id + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "FAILURE") + self.assertFalse(output['in_progress']) + self.assertEquals(output['message'], TEST_FAILURE_MESSAGE) + + def test_initial_success(self): + course_task_log = self._create_success_entry() + task_id = course_task_log.task_id + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "SUCCESS") + self.assertFalse(output['in_progress']) + + def test_update_progress_to_progress(self): + # view task entry for task in progress + course_task_log = self._create_progress_entry() + task_id = course_task_log.task_id + mock_result = Mock() + mock_result.task_id = task_id + mock_result.state = "PROGRESS" + mock_result.result = {'attempted': 5, + 'updated': 4, + 'total': 10, + 'action_name': 'regraded'} + with patch('celery.result.AsyncResult.__new__') as mock_result_ctor: + mock_result_ctor.return_value = mock_result + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "PROGRESS") + self.assertTrue(output['in_progress']) + # self.assertEquals(output['message'], ) + + def test_update_progress_to_failure(self): + # view task entry for task in progress that later fails + course_task_log = self._create_progress_entry() + task_id = course_task_log.task_id + mock_result = Mock() + mock_result.task_id = task_id + mock_result.state = "FAILURE" + mock_result.result = NotImplementedError("This task later failed.") + mock_result.traceback = "random traceback" + with patch('celery.result.AsyncResult.__new__') as mock_result_ctor: + mock_result_ctor.return_value = mock_result + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "FAILURE") + self.assertFalse(output['in_progress']) + self.assertEquals(output['message'], "This task later failed.") + + def test_update_progress_to_revoked(self): + # view task entry for task in progress that later fails + course_task_log = self._create_progress_entry() + task_id = course_task_log.task_id + mock_result = Mock() + mock_result.task_id = task_id + mock_result.state = "REVOKED" + with patch('celery.result.AsyncResult.__new__') as mock_result_ctor: + mock_result_ctor.return_value = mock_result + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "REVOKED") + self.assertFalse(output['in_progress']) + self.assertEquals(output['message'], "Task revoked before running") + + def _get_output_for_task_success(self, attempted, updated, total, student=None): + # view task entry for task in progress + course_task_log = self._create_progress_entry(student) + task_id = course_task_log.task_id + mock_result = Mock() + mock_result.task_id = task_id + mock_result.state = "SUCCESS" + mock_result.result = {'attempted': attempted, + 'updated': updated, + 'total': total, + 'action_name': 'regraded'} + with patch('celery.result.AsyncResult.__new__') as mock_result_ctor: + mock_result_ctor.return_value = mock_result + response = course_task_log_status(Mock(), task_id=task_id) + output = json.loads(response.content) + return task_id, output + + def test_update_progress_to_success(self): + task_id, output = self._get_output_for_task_success(10, 8, 10) + self.assertEquals(output['task_id'], task_id) + self.assertEquals(output['task_state'], "SUCCESS") + self.assertFalse(output['in_progress']) + + def test_success_messages(self): + _, output = self._get_output_for_task_success(0, 0, 10) + self.assertTrue("Unable to find any students with submissions to be regraded" in output['message']) + self.assertFalse(output['succeeded']) + + _, output = self._get_output_for_task_success(10, 0, 10) + self.assertTrue("Problem failed to be regraded for any of 10 students " in output['message']) + self.assertFalse(output['succeeded']) + + _, output = self._get_output_for_task_success(10, 8, 10) + self.assertTrue("Problem regraded for 8 of 10 students" in output['message']) + self.assertFalse(output['succeeded']) + + _, output = self._get_output_for_task_success(10, 10, 10) + self.assertTrue("Problem successfully regraded for 10 students" in output['message']) + self.assertTrue(output['succeeded']) + + _, output = self._get_output_for_task_success(0, 0, 1, student=self.student) + self.assertTrue("Unable to find submission to be regraded for student" in output['message']) + self.assertFalse(output['succeeded']) + + _, output = self._get_output_for_task_success(1, 0, 1, student=self.student) + self.assertTrue("Problem failed to be regraded for student" in output['message']) + self.assertFalse(output['succeeded']) + + _, output = self._get_output_for_task_success(1, 1, 1, student=self.student) + self.assertTrue("Problem successfully regraded for student" in output['message']) + self.assertTrue(output['succeeded']) + + def test_submit_nonexistent_modules(self): + # confirm that a regrade of a non-existent module returns an exception + # (Note that it is easier to test a non-regradable module in test_tasks, + # where we are creating real modules. + problem_url = self.problem_url + course_id = "something else" + request = None + with self.assertRaises(ItemNotFoundError): + submit_regrade_problem_for_student(request, course_id, problem_url, self.student) + with self.assertRaises(ItemNotFoundError): + submit_regrade_problem_for_all_students(request, course_id, problem_url) + with self.assertRaises(ItemNotFoundError): + submit_reset_problem_attempts_for_all_students(request, course_id, problem_url) + with self.assertRaises(ItemNotFoundError): + submit_delete_problem_state_for_all_students(request, course_id, problem_url) + + def test_submit_when_running(self): + # get exception when trying to submit a task that is already running + course_task_log = self._create_progress_entry() + problem_url = course_task_log.task_args + course_id = course_task_log.course_id + # requester doesn't have to be the same when determining if a task is already running + request = Mock() + request.user = self.student + with self.assertRaises(AlreadyRunningError): + # just skip making the argument check, so we don't have to fake it deeper down + with patch('courseware.task_queue._check_arguments_for_regrading'): + submit_regrade_problem_for_all_students(request, course_id, problem_url) diff --git a/lms/djangoapps/courseware/tests/test_tasks.py b/lms/djangoapps/courseware/tests/test_tasks.py new file mode 100644 index 0000000000..51f5f4cffc --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_tasks.py @@ -0,0 +1,340 @@ +''' +Test for LMS courseware background tasks +''' +import logging +import json +from mock import Mock, patch + +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test.utils import override_settings + +from capa.tests.response_xml_factory import OptionResponseXMLFactory, CodeResponseXMLFactory +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.exceptions import ItemNotFoundError +from student.tests.factories import CourseEnrollmentFactory, UserFactory, AdminFactory + +from courseware.model_data import StudentModule +from courseware.task_queue import (submit_regrade_problem_for_all_students, + submit_regrade_problem_for_student, + course_task_log_status) +from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE + + +log = logging.getLogger("mitx." + __name__) + + +TEST_COURSE_ORG = 'edx' +TEST_COURSE_NAME = 'Test Course' +TEST_COURSE_NUMBER = '1.23x' +TEST_SECTION_NAME = "Problem" + + +@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +class TestRegradingBase(LoginEnrollmentTestCase, ModuleStoreTestCase): + """ + Test that all students' answers to a problem can be regraded after the + definition of the problem has been redefined. + """ + course = None + current_user = None + + def initialize_course(self): + """Create a course in the store, with a chapter and section.""" + self.module_store = modulestore() + + # Create the course + self.course = CourseFactory.create(org=TEST_COURSE_ORG, + number=TEST_COURSE_NUMBER, + display_name=TEST_COURSE_NAME) + + # Add a chapter to the course + chapter = ItemFactory.create(parent_location=self.course.location, + display_name=TEST_SECTION_NAME) + + # add a sequence to the course to which the problems can be added + self.problem_section = ItemFactory.create(parent_location=chapter.location, + template='i4x://edx/templates/sequential/Empty', + display_name=TEST_SECTION_NAME) + + @staticmethod + def get_user_email(username): + return '{0}@test.com'.format(username) + + @staticmethod + def get_user_password(username): + return 'test' + + def login_username(self, username): + self.login(TestRegradingBase.get_user_email(username), TestRegradingBase.get_user_password(username)) + self.current_user = username + + def _create_user(self, username, is_staff=False): + email = TestRegradingBase.get_user_email(username) + if (is_staff): + AdminFactory.create(username=username, email=email) + else: + UserFactory.create(username=username, email=email) + thisuser = User.objects.get(username=username) + CourseEnrollmentFactory.create(user=thisuser, course_id=self.course.id) + return thisuser + + def create_instructor(self, username): + return self._create_user(username, is_staff=True) + + def create_student(self, username): + return self._create_user(username, is_staff=False) + + @staticmethod + def problem_location(problem_url_name): + """ + Create an internal location for a test problem. + """ + if "i4x:" in problem_url_name: + return problem_url_name + else: + return "i4x://{org}/{number}/problem/{problem_url_name}".format(org=TEST_COURSE_ORG, + number=TEST_COURSE_NUMBER, + problem_url_name=problem_url_name) + + def define_option_problem(self, problem_url_name): + """Create the problem definition so the answer is Option 1""" + factory = OptionResponseXMLFactory() + factory_args = {'question_text': 'The correct answer is Option 1', + 'options': ['Option 1', 'Option 2'], + 'correct_option': 'Option 1', + 'num_responses': 2} + problem_xml = factory.build_xml(**factory_args) + ItemFactory.create(parent_location=self.problem_section.location, + template="i4x://edx/templates/problem/Blank_Common_Problem", + display_name=str(problem_url_name), + data=problem_xml) + + def redefine_option_problem(self, problem_url_name): + """Change the problem definition so the answer is Option 2""" + factory = OptionResponseXMLFactory() + factory_args = {'question_text': 'The correct answer is Option 2', + 'options': ['Option 1', 'Option 2'], + 'correct_option': 'Option 2', + 'num_responses': 2} + problem_xml = factory.build_xml(**factory_args) + location = TestRegrading.problem_location(problem_url_name) + self.module_store.update_item(location, problem_xml) + + def render_problem(self, username, problem_url_name): + """ + Use ajax interface to request html for a problem. + """ + # make sure that the requested user is logged in, so that the ajax call works + # on the right problem: + if self.current_user != username: + self.login_username(username) + # make ajax call: + modx_url = reverse('modx_dispatch', + kwargs={ + 'course_id': self.course.id, + 'location': TestRegrading.problem_location(problem_url_name), + 'dispatch': 'problem_get', }) + resp = self.client.post(modx_url, {}) + return resp + + def submit_student_answer(self, username, problem_url_name, responses): + """ + Use ajax interface to submit a student answer. + + Assumes the input list of responses has two values. + """ + def get_input_id(response_id): + return 'input_i4x-{0}-{1}-problem-{2}_{3}'.format(TEST_COURSE_ORG.lower(), + TEST_COURSE_NUMBER.replace('.', '_'), + problem_url_name, response_id) + + # make sure that the requested user is logged in, so that the ajax call works + # on the right problem: + if self.current_user != username: + self.login_username(username) + # make ajax call: + modx_url = reverse('modx_dispatch', + kwargs={ + 'course_id': self.course.id, + 'location': TestRegrading.problem_location(problem_url_name), + 'dispatch': 'problem_check', }) + + resp = self.client.post(modx_url, { + get_input_id('2_1'): responses[0], + get_input_id('3_1'): responses[1], + }) + return resp + + def _create_task_request(self, requester_username): + """Generate request that can be used for submitting tasks""" + request = Mock() + request.user = User.objects.get(username=requester_username) + request.get_host = Mock(return_value="testhost") + request.META = {'REMOTE_ADDR': '0:0:0:0', 'SERVER_NAME': 'testhost'} + request.is_secure = Mock(return_value=False) + return request + + def regrade_all_student_answers(self, instructor, problem_url_name): + """Submits the current problem for regrading""" + return submit_regrade_problem_for_all_students(self._create_task_request(instructor), self.course.id, + TestRegradingBase.problem_location(problem_url_name)) + + def regrade_one_student_answer(self, instructor, problem_url_name, student): + """Submits the current problem for regrading for a particular student""" + return submit_regrade_problem_for_student(self._create_task_request(instructor), self.course.id, + TestRegradingBase.problem_location(problem_url_name), + student) + + def show_correct_answer(self, problem_url_name): + modx_url = reverse('modx_dispatch', + kwargs={ + 'course_id': self.course.id, + 'location': TestRegradingBase.problem_location(problem_url_name), + 'dispatch': 'problem_show', }) + return self.client.post(modx_url, {}) + + def get_student_module(self, username, descriptor): + return StudentModule.objects.get(course_id=self.course.id, + student=User.objects.get(username=username), + module_type=descriptor.location.category, + module_state_key=descriptor.location.url(), + ) + + def check_state(self, username, descriptor, expected_score, expected_max_score, expected_attempts): + module = self.get_student_module(username, descriptor) + self.assertEqual(module.grade, expected_score, "Scores were not equal") + self.assertEqual(module.max_grade, expected_max_score, "Max scores were not equal") + state = json.loads(module.state) + attempts = state['attempts'] + self.assertEqual(attempts, expected_attempts, "Attempts were not equal") + if attempts > 0: + self.assertTrue('correct_map' in state) + self.assertTrue('student_answers' in state) + self.assertGreater(len(state['correct_map']), 0) + self.assertGreater(len(state['student_answers']), 0) + + +class TestRegrading(TestRegradingBase): + + def setUp(self): + self.initialize_course() + self.create_instructor('instructor') + self.create_student('u1') + self.create_student('u2') + self.create_student('u3') + self.create_student('u4') + self.logout() + + def testRegradingOptionProblem(self): + '''Run regrade scenario on option problem''' + # get descriptor: + problem_url_name = 'H1P1' + self.define_option_problem(problem_url_name) + location = TestRegrading.problem_location(problem_url_name) + descriptor = self.module_store.get_instance(self.course.id, location) + + # first store answers for each of the separate users: + self.submit_student_answer('u1', problem_url_name, ['Option 1', 'Option 1']) + self.submit_student_answer('u2', problem_url_name, ['Option 1', 'Option 2']) + self.submit_student_answer('u3', problem_url_name, ['Option 2', 'Option 1']) + self.submit_student_answer('u4', problem_url_name, ['Option 2', 'Option 2']) + + self.check_state('u1', descriptor, 2, 2, 1) + self.check_state('u2', descriptor, 1, 2, 1) + self.check_state('u3', descriptor, 1, 2, 1) + self.check_state('u4', descriptor, 0, 2, 1) + + # update the data in the problem definition + self.redefine_option_problem(problem_url_name) + # confirm that simply rendering the problem again does not result in a change + # in the grade: + self.render_problem('u1', problem_url_name) + self.check_state('u1', descriptor, 2, 2, 1) + + # regrade the problem for only one student -- only that student's grade should change: + self.regrade_one_student_answer('instructor', problem_url_name, User.objects.get(username='u1')) + self.check_state('u1', descriptor, 0, 2, 1) + self.check_state('u2', descriptor, 1, 2, 1) + self.check_state('u3', descriptor, 1, 2, 1) + self.check_state('u4', descriptor, 0, 2, 1) + + # regrade the problem for all students + self.regrade_all_student_answers('instructor', problem_url_name) + self.check_state('u1', descriptor, 0, 2, 1) + self.check_state('u2', descriptor, 1, 2, 1) + self.check_state('u3', descriptor, 1, 2, 1) + self.check_state('u4', descriptor, 2, 2, 1) + + def define_code_response_problem(self, problem_url_name): + factory = CodeResponseXMLFactory() + grader_payload = json.dumps({"grader": "ps04/grade_square.py"}) + problem_xml = factory.build_xml(initial_display="def square(x):", + answer_display="answer", + grader_payload=grader_payload, + num_responses=2) + ItemFactory.create(parent_location=self.problem_section.location, + template="i4x://edx/templates/problem/Blank_Common_Problem", + display_name=str(problem_url_name), + data=problem_xml) + + def testRegradingFailure(self): + """Simulate a failure in regrading a problem""" + problem_url_name = 'H1P1' + self.define_option_problem(problem_url_name) + self.submit_student_answer('u1', problem_url_name, ['Option 1', 'Option 1']) + + expected_message = "bad things happened" + with patch('capa.capa_problem.LoncapaProblem.regrade_existing_answers') as mock_regrade: + mock_regrade.side_effect = ZeroDivisionError(expected_message) + course_task_log = self.regrade_all_student_answers('instructor', problem_url_name) + + # check task_log returned + self.assertEqual(course_task_log.task_state, 'FAILURE') + self.assertEqual(course_task_log.student, None) + self.assertEqual(course_task_log.requester.username, 'instructor') + self.assertEqual(course_task_log.task_name, 'regrade_problem') + self.assertEqual(course_task_log.task_args, TestRegrading.problem_location(problem_url_name)) + status = json.loads(course_task_log.task_progress) + self.assertEqual(status['exception'], 'ZeroDivisionError') + self.assertEqual(status['message'], expected_message) + + # check status returned: + mock_request = Mock() + response = course_task_log_status(mock_request, task_id=course_task_log.task_id) + status = json.loads(response.content) + self.assertEqual(status['message'], expected_message) + + def testRegradingNonProblem(self): + """confirm that a non-problem will not submit""" + problem_url_name = self.problem_section.location.url() + with self.assertRaises(NotImplementedError): + self.regrade_all_student_answers('instructor', problem_url_name) + + def testRegradingNonexistentProblem(self): + """confirm that a non-existent problem will not submit""" + problem_url_name = 'NonexistentProblem' + with self.assertRaises(ItemNotFoundError): + self.regrade_all_student_answers('instructor', problem_url_name) + + def testRegradingCodeProblem(self): + '''Run regrade scenario on problem with code submission''' + problem_url_name = 'H1P2' + self.define_code_response_problem(problem_url_name) + # we fully create the CodeResponse problem, but just pretend that we're queuing it: + with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: + mock_send_to_queue.return_value = (0, "Successfully queued") + self.submit_student_answer('u1', problem_url_name, ["answer1", "answer2"]) + + course_task_log = self.regrade_all_student_answers('instructor', problem_url_name) + self.assertEqual(course_task_log.task_state, 'FAILURE') + status = json.loads(course_task_log.task_progress) + self.assertEqual(status['exception'], 'NotImplementedError') + self.assertEqual(status['message'], "Problem's definition does not support regrading") + + mock_request = Mock() + response = course_task_log_status(mock_request, task_id=course_task_log.task_id) + status = json.loads(response.content) + self.assertEqual(status['message'], "Problem's definition does not support regrading")