test_api.py 16.4 KB
Newer Older
1
"""
2
Test for LMS instructor background task queue management
3
"""
4
import ddt
5
from mock import MagicMock, Mock, patch
6
from nose.plugins.attrib import attr
Brian Wilson committed
7

8 9
from bulk_email.models import SEND_TO_LEARNERS, SEND_TO_MYSELF, SEND_TO_STAFF, CourseEmail
from certificates.models import CertificateGenerationHistory, CertificateStatuses
Jeremy Bowman committed
10
from common.test.utils import normalize_repr
11
from courseware.tests.factories import UserFactory
12
from lms.djangoapps.instructor_task.api import (
13 14
    SpecificStudentIdMissingError,
    generate_certificates_for_students,
15
    get_instructor_task_history,
16 17
    get_running_instructor_tasks,
    regenerate_certificates,
18
    submit_bulk_course_email,
19
    submit_calculate_may_enroll_csv,
20
    submit_calculate_problem_responses_csv,
21
    submit_calculate_students_features_csv,
22
    submit_cohort_students,
23 24 25
    submit_course_survey_report,
    submit_delete_entrance_exam_state_for_student,
    submit_delete_problem_state_for_all_students,
26
    submit_detailed_enrollment_features_csv,
27
    submit_executive_summary_report,
28
    submit_export_ora2_data,
29
    submit_override_score,
30 31 32 33 34
    submit_rescore_entrance_exam_for_student,
    submit_rescore_problem_for_all_students,
    submit_rescore_problem_for_student,
    submit_reset_problem_attempts_for_all_students,
    submit_reset_problem_attempts_in_entrance_exam
35
)
36
from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError, QueueConnectionError
37
from lms.djangoapps.instructor_task.models import PROGRESS, InstructorTask
38 39
from lms.djangoapps.instructor_task.tasks import export_ora2_data
from lms.djangoapps.instructor_task.tests.test_base import (
40
    TEST_COURSE_KEY,
41 42
    InstructorTaskCourseTestCase,
    InstructorTaskModuleTestCase,
43 44
    InstructorTaskTestCase,
    TestReportMixin
45
)
46
from xmodule.modulestore.exceptions import ItemNotFoundError
47
from celery.states import FAILURE
48 49


50
class InstructorTaskReportTest(InstructorTaskTestCase):
51
    """
52
    Tests API methods that involve the reporting of status for background tasks.
53 54
    """

55
    def test_get_running_instructor_tasks(self):
56
        # when fetching running tasks, we get all running tasks, and only running tasks
57 58 59 60
        for _ in range(1, 5):
            self._create_failure_entry()
            self._create_success_entry()
        progress_task_ids = [self._create_progress_entry().task_id for _ in range(1, 5)]
61
        task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_KEY)]
62 63
        self.assertEquals(set(task_ids), set(progress_task_ids))

Brian Wilson committed
64 65 66 67 68 69 70 71
    def test_get_instructor_task_history(self):
        # when fetching historical tasks, we get all tasks, including running tasks
        expected_ids = []
        for _ in range(1, 5):
            expected_ids.append(self._create_failure_entry().task_id)
            expected_ids.append(self._create_success_entry().task_id)
            expected_ids.append(self._create_progress_entry().task_id)
        task_ids = [instructor_task.task_id for instructor_task
72
                    in get_instructor_task_history(TEST_COURSE_KEY, usage_key=self.problem_url)]
Brian Wilson committed
73
        self.assertEquals(set(task_ids), set(expected_ids))
74 75 76
        # make the same call using explicit task_type:
        task_ids = [instructor_task.task_id for instructor_task
                    in get_instructor_task_history(
77 78
                        TEST_COURSE_KEY,
                        usage_key=self.problem_url,
79 80 81 82 83 84
                        task_type='rescore_problem'
                    )]
        self.assertEquals(set(task_ids), set(expected_ids))
        # make the same call using a non-existent task_type:
        task_ids = [instructor_task.task_id for instructor_task
                    in get_instructor_task_history(
85 86
                        TEST_COURSE_KEY,
                        usage_key=self.problem_url,
87 88 89
                        task_type='dummy_type'
                    )]
        self.assertEquals(set(task_ids), set())
Brian Wilson committed
90

91

92
@attr(shard=3)
93
@ddt.ddt
94 95
class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
    """Tests API methods that involve the submission of module-based background tasks."""
Brian Wilson committed
96 97

    def setUp(self):
98 99
        super(InstructorTaskModuleSubmitTest, self).setUp()

Brian Wilson committed
100 101 102 103
        self.initialize_course()
        self.student = UserFactory.create(username="student", email="student@edx.org")
        self.instructor = UserFactory.create(username="instructor", email="instructor@edx.org")

104
    def test_submit_nonexistent_modules(self):
105
        # confirm that a rescore of a non-existent module returns an exception
106
        problem_url = InstructorTaskModuleTestCase.problem_location("NonexistentProblem")
107 108
        request = None
        with self.assertRaises(ItemNotFoundError):
109
            submit_rescore_problem_for_student(request, problem_url, self.student)
110
        with self.assertRaises(ItemNotFoundError):
111
            submit_rescore_problem_for_all_students(request, problem_url)
112
        with self.assertRaises(ItemNotFoundError):
113
            submit_reset_problem_attempts_for_all_students(request, problem_url)
114
        with self.assertRaises(ItemNotFoundError):
115
            submit_delete_problem_state_for_all_students(request, problem_url)
116

Brian Wilson committed
117
    def test_submit_nonrescorable_modules(self):
118
        # confirm that a rescore of an existent but unscorable module returns an exception
119
        # (Note that it is easier to test a scoreable but non-rescorable module in test_tasks,
120
        # where we are creating real modules.)
121
        problem_url = self.problem_section.location
Brian Wilson committed
122 123
        request = None
        with self.assertRaises(NotImplementedError):
124
            submit_rescore_problem_for_student(request, problem_url, self.student)
Brian Wilson committed
125
        with self.assertRaises(NotImplementedError):
126
            submit_rescore_problem_for_all_students(request, problem_url)
Brian Wilson committed
127

128
    def _test_submit_with_long_url(self, task_function, student=None):
129 130
        problem_url_name = 'x' * 255
        self.define_option_problem(problem_url_name)
131
        location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
132 133
        with self.assertRaises(ValueError):
            if student is not None:
134
                task_function(self.create_task_request(self.instructor), location, student)
135
            else:
136
                task_function(self.create_task_request(self.instructor), location)
137 138 139 140 141 142 143 144 145 146 147 148 149

    def test_submit_rescore_all_with_long_url(self):
        self._test_submit_with_long_url(submit_rescore_problem_for_all_students)

    def test_submit_rescore_student_with_long_url(self):
        self._test_submit_with_long_url(submit_rescore_problem_for_student, self.student)

    def test_submit_reset_all_with_long_url(self):
        self._test_submit_with_long_url(submit_reset_problem_attempts_for_all_students)

    def test_submit_delete_all_with_long_url(self):
        self._test_submit_with_long_url(submit_delete_problem_state_for_all_students)

150
    @ddt.data(
Jeremy Bowman committed
151
        (normalize_repr(submit_rescore_problem_for_all_students), 'rescore_problem'),
152
        (
Jeremy Bowman committed
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
            normalize_repr(submit_rescore_problem_for_all_students),
            'rescore_problem_if_higher',
            {'only_if_higher': True}
        ),
        (normalize_repr(submit_rescore_problem_for_student), 'rescore_problem', {'student': True}),
        (
            normalize_repr(submit_rescore_problem_for_student),
            'rescore_problem_if_higher',
            {'student': True, 'only_if_higher': True}
        ),
        (normalize_repr(submit_reset_problem_attempts_for_all_students), 'reset_problem_attempts'),
        (normalize_repr(submit_delete_problem_state_for_all_students), 'delete_problem_state'),
        (normalize_repr(submit_rescore_entrance_exam_for_student), 'rescore_problem', {'student': True}),
        (
            normalize_repr(submit_rescore_entrance_exam_for_student),
168 169 170
            'rescore_problem_if_higher',
            {'student': True, 'only_if_higher': True},
        ),
Jeremy Bowman committed
171 172 173
        (normalize_repr(submit_reset_problem_attempts_in_entrance_exam), 'reset_problem_attempts', {'student': True}),
        (normalize_repr(submit_delete_entrance_exam_state_for_student), 'delete_problem_state', {'student': True}),
        (normalize_repr(submit_override_score), 'override_problem_score', {'student': True, 'score': 0})
174 175 176
    )
    @ddt.unpack
    def test_submit_task(self, task_function, expected_task_type, params=None):
177 178 179
        """
        Tests submission of instructor task.
        """
180 181 182 183 184
        if params is None:
            params = {}
        if params.get('student'):
            params['student'] = self.student

Brian Wilson committed
185 186
        problem_url_name = 'H1P1'
        self.define_option_problem(problem_url_name)
187
        location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
188 189 190 191 192 193 194 195 196 197 198 199 200 201

        # unsuccessful submission, exception raised while submitting.
        with patch('lms.djangoapps.instructor_task.tasks_base.BaseInstructorTask.apply_async') as apply_async:

            error = Exception()
            apply_async.side_effect = error

            with self.assertRaises(QueueConnectionError):
                instructor_task = task_function(self.create_task_request(self.instructor), location, **params)

            most_recent_task = InstructorTask.objects.latest('id')
            self.assertEquals(most_recent_task.task_state, FAILURE)

        # successful submission
202 203
        instructor_task = task_function(self.create_task_request(self.instructor), location, **params)
        self.assertEquals(instructor_task.task_type, expected_task_type)
Brian Wilson committed
204 205 206 207 208 209 210

        # test resubmitting, by updating the existing record:
        instructor_task = InstructorTask.objects.get(id=instructor_task.id)
        instructor_task.task_state = PROGRESS
        instructor_task.save()

        with self.assertRaises(AlreadyRunningError):
211
            task_function(self.create_task_request(self.instructor), location, **params)
212 213


214
@attr(shard=3)
215
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
216
class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCase):
217 218 219
    """Tests API methods that involve the submission of course-based background tasks."""

    def setUp(self):
220 221
        super(InstructorTaskCourseSubmitTest, self).setUp()

222 223 224 225 226
        self.initialize_course()
        self.student = UserFactory.create(username="student", email="student@edx.org")
        self.instructor = UserFactory.create(username="instructor", email="instructor@edx.org")

    def _define_course_email(self):
227
        """Create CourseEmail object for testing."""
228 229 230 231 232 233 234
        course_email = CourseEmail.create(
            self.course.id,
            self.instructor,
            [SEND_TO_MYSELF, SEND_TO_STAFF, SEND_TO_LEARNERS],
            "Test Subject",
            "<p>This is a test message</p>"
        )
235
        return course_email.id
236

237 238 239 240 241 242 243 244 245
    def _test_resubmission(self, api_call):
        """
        Tests the resubmission of an instructor task through the API.
        The call to the API is a lambda expression passed via
        `api_call`.  Expects that the API call returns the resulting
        InstructorTask object, and that its resubmission raises
        `AlreadyRunningError`.
        """
        instructor_task = api_call()
246
        instructor_task = InstructorTask.objects.get(id=instructor_task.id)
247 248 249
        instructor_task.task_state = PROGRESS
        instructor_task.save()
        with self.assertRaises(AlreadyRunningError):
250 251 252 253 254 255 256 257 258 259 260
            api_call()

    def test_submit_bulk_email_all(self):
        email_id = self._define_course_email()
        api_call = lambda: submit_bulk_course_email(
            self.create_task_request(self.instructor),
            self.course.id,
            email_id
        )
        self._test_resubmission(api_call)

261 262 263 264 265 266 267 268
    def test_submit_calculate_problem_responses(self):
        api_call = lambda: submit_calculate_problem_responses_csv(
            self.create_task_request(self.instructor),
            self.course.id,
            problem_location=''
        )
        self._test_resubmission(api_call)

269 270 271 272 273 274 275
    def test_submit_calculate_students_features(self):
        api_call = lambda: submit_calculate_students_features_csv(
            self.create_task_request(self.instructor),
            self.course.id,
            features=[]
        )
        self._test_resubmission(api_call)
276

277 278 279 280 281
    def test_submit_enrollment_report_features_csv(self):
        api_call = lambda: submit_detailed_enrollment_features_csv(self.create_task_request(self.instructor),
                                                                   self.course.id)
        self._test_resubmission(api_call)

Afzal Wali committed
282 283 284 285 286 287
    def test_submit_executive_summary_report(self):
        api_call = lambda: submit_executive_summary_report(
            self.create_task_request(self.instructor), self.course.id
        )
        self._test_resubmission(api_call)

288 289 290 291 292 293
    def test_submit_course_survey_report(self):
        api_call = lambda: submit_course_survey_report(
            self.create_task_request(self.instructor), self.course.id
        )
        self._test_resubmission(api_call)

294 295 296 297 298 299 300 301
    def test_submit_calculate_may_enroll(self):
        api_call = lambda: submit_calculate_may_enroll_csv(
            self.create_task_request(self.instructor),
            self.course.id,
            features=[]
        )
        self._test_resubmission(api_call)

302 303 304 305 306 307 308
    def test_submit_cohort_students(self):
        api_call = lambda: submit_cohort_students(
            self.create_task_request(self.instructor),
            self.course.id,
            file_name=u'filename.csv'
        )
        self._test_resubmission(api_call)
309

310 311 312
    def test_submit_ora2_request_task(self):
        request = self.create_task_request(self.instructor)

313
        with patch('lms.djangoapps.instructor_task.api.submit_task') as mock_submit_task:
314 315 316 317 318 319
            mock_submit_task.return_value = MagicMock()
            submit_export_ora2_data(request, self.course.id)

            mock_submit_task.assert_called_once_with(
                request, 'export_ora2_data', export_ora2_data, self.course.id, {}, '')

320 321 322 323
    def test_submit_generate_certs_students(self):
        """
        Tests certificates generation task submission api
        """
324
        api_call = lambda: generate_certificates_for_students(
325 326 327 328
            self.create_task_request(self.instructor),
            self.course.id
        )
        self._test_resubmission(api_call)
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

    def test_regenerate_certificates(self):
        """
        Tests certificates regeneration task submission api
        """
        def api_call():
            """
            wrapper method for regenerate_certificates
            """
            return regenerate_certificates(
                self.create_task_request(self.instructor),
                self.course.id,
                [CertificateStatuses.downloadable, CertificateStatuses.generating]
            )
        self._test_resubmission(api_call)
344

345 346 347 348 349 350 351 352 353 354 355 356
    def test_certificate_generation_no_specific_student_id(self):
        """
        Raises ValueError when student_set is 'specific_student' and 'specific_student_id' is None.
        """
        with self.assertRaises(SpecificStudentIdMissingError):
            generate_certificates_for_students(
                self.create_task_request(self.instructor),
                self.course.id,
                student_set='specific_student',
                specific_student_id=None
            )

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
    def test_certificate_generation_history(self):
        """
        Tests that a new record is added whenever certificate generation/regeneration task is submitted.
        """
        instructor_task = generate_certificates_for_students(
            self.create_task_request(self.instructor),
            self.course.id
        )
        certificate_generation_history = CertificateGenerationHistory.objects.filter(
            course_id=self.course.id,
            generated_by=self.instructor,
            instructor_task=instructor_task,
            is_regeneration=False
        )

        # Validate that record was added to CertificateGenerationHistory
        self.assertTrue(certificate_generation_history.exists())

        instructor_task = regenerate_certificates(
            self.create_task_request(self.instructor),
            self.course.id,
            [CertificateStatuses.downloadable, CertificateStatuses.generating]
        )
        certificate_generation_history = CertificateGenerationHistory.objects.filter(
            course_id=self.course.id,
            generated_by=self.instructor,
            instructor_task=instructor_task,
            is_regeneration=True
        )

        # Validate that record was added to CertificateGenerationHistory
        self.assertTrue(certificate_generation_history.exists())