test_api.py 14.6 KB
Newer Older
1
"""
2
Test for LMS instructor background task queue management
3
"""
4
from mock import patch, Mock, MagicMock
5
from nose.plugins.attrib import attr
6
from bulk_email.models import CourseEmail, SEND_TO_MYSELF, SEND_TO_STAFF, SEND_TO_LEARNERS
7
from courseware.tests.factories import UserFactory
8
from xmodule.modulestore.exceptions import ItemNotFoundError
Brian Wilson committed
9

10 11 12 13 14 15 16 17
from instructor_task.api import (
    get_running_instructor_tasks,
    get_instructor_task_history,
    submit_rescore_problem_for_all_students,
    submit_rescore_problem_for_student,
    submit_reset_problem_attempts_for_all_students,
    submit_delete_problem_state_for_all_students,
    submit_bulk_course_email,
18
    submit_calculate_problem_responses_csv,
19
    submit_calculate_students_features_csv,
20
    submit_cohort_students,
21 22
    submit_detailed_enrollment_features_csv,
    submit_calculate_may_enroll_csv,
23
    submit_executive_summary_report,
24
    submit_course_survey_report,
25
    generate_certificates_for_students,
26 27
    regenerate_certificates,
    submit_export_ora2_data,
28
    SpecificStudentIdMissingError,
29
)
30

31 32
from instructor_task.api_helper import AlreadyRunningError
from instructor_task.models import InstructorTask, PROGRESS
33 34 35 36 37 38 39 40
from instructor_task.tasks import export_ora2_data
from instructor_task.tests.test_base import (
    InstructorTaskTestCase,
    InstructorTaskCourseTestCase,
    InstructorTaskModuleTestCase,
    TestReportMixin,
    TEST_COURSE_KEY,
)
41
from certificates.models import CertificateStatuses, CertificateGenerationHistory
42 43


44
class InstructorTaskReportTest(InstructorTaskTestCase):
45
    """
46
    Tests API methods that involve the reporting of status for background tasks.
47 48
    """

49
    def test_get_running_instructor_tasks(self):
50
        # when fetching running tasks, we get all running tasks, and only running tasks
51 52 53 54
        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)]
55
        task_ids = [instructor_task.task_id for instructor_task in get_running_instructor_tasks(TEST_COURSE_KEY)]
56 57
        self.assertEquals(set(task_ids), set(progress_task_ids))

Brian Wilson committed
58 59 60 61 62 63 64 65
    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
66
                    in get_instructor_task_history(TEST_COURSE_KEY, usage_key=self.problem_url)]
Brian Wilson committed
67
        self.assertEquals(set(task_ids), set(expected_ids))
68 69 70
        # make the same call using explicit task_type:
        task_ids = [instructor_task.task_id for instructor_task
                    in get_instructor_task_history(
71 72
                        TEST_COURSE_KEY,
                        usage_key=self.problem_url,
73 74 75 76 77 78
                        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(
79 80
                        TEST_COURSE_KEY,
                        usage_key=self.problem_url,
81 82 83
                        task_type='dummy_type'
                    )]
        self.assertEquals(set(task_ids), set())
Brian Wilson committed
84

85

86
@attr(shard=3)
87 88
class InstructorTaskModuleSubmitTest(InstructorTaskModuleTestCase):
    """Tests API methods that involve the submission of module-based background tasks."""
Brian Wilson committed
89 90

    def setUp(self):
91 92
        super(InstructorTaskModuleSubmitTest, self).setUp()

Brian Wilson committed
93 94 95 96
        self.initialize_course()
        self.student = UserFactory.create(username="student", email="student@edx.org")
        self.instructor = UserFactory.create(username="instructor", email="instructor@edx.org")

97
    def test_submit_nonexistent_modules(self):
98
        # confirm that a rescore of a non-existent module returns an exception
99
        problem_url = InstructorTaskModuleTestCase.problem_location("NonexistentProblem")
100 101
        request = None
        with self.assertRaises(ItemNotFoundError):
102
            submit_rescore_problem_for_student(request, problem_url, self.student)
103
        with self.assertRaises(ItemNotFoundError):
104
            submit_rescore_problem_for_all_students(request, problem_url)
105
        with self.assertRaises(ItemNotFoundError):
106
            submit_reset_problem_attempts_for_all_students(request, problem_url)
107
        with self.assertRaises(ItemNotFoundError):
108
            submit_delete_problem_state_for_all_students(request, problem_url)
109

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

121
    def _test_submit_with_long_url(self, task_function, student=None):
122 123
        problem_url_name = 'x' * 255
        self.define_option_problem(problem_url_name)
124
        location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
125 126
        with self.assertRaises(ValueError):
            if student is not None:
127
                task_function(self.create_task_request(self.instructor), location, student)
128
            else:
129
                task_function(self.create_task_request(self.instructor), location)
130 131 132 133 134 135 136 137 138 139 140 141 142

    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)

143 144
    def _test_submit_task(self, task_function, student=None):
        # tests submit, and then tests a second identical submission.
Brian Wilson committed
145 146
        problem_url_name = 'H1P1'
        self.define_option_problem(problem_url_name)
147
        location = InstructorTaskModuleTestCase.problem_location(problem_url_name)
Brian Wilson committed
148
        if student is not None:
149
            instructor_task = task_function(self.create_task_request(self.instructor), location, student)
Brian Wilson committed
150
        else:
151
            instructor_task = task_function(self.create_task_request(self.instructor), location)
Brian Wilson committed
152 153 154 155 156 157 158 159

        # 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):
            if student is not None:
160
                task_function(self.create_task_request(self.instructor), location, student)
Brian Wilson committed
161
            else:
162
                task_function(self.create_task_request(self.instructor), location)
Brian Wilson committed
163 164 165 166 167 168 169 170 171 172 173 174

    def test_submit_rescore_all(self):
        self._test_submit_task(submit_rescore_problem_for_all_students)

    def test_submit_rescore_student(self):
        self._test_submit_task(submit_rescore_problem_for_student, self.student)

    def test_submit_reset_all(self):
        self._test_submit_task(submit_reset_problem_attempts_for_all_students)

    def test_submit_delete_all(self):
        self._test_submit_task(submit_delete_problem_state_for_all_students)
175 176


177
@attr(shard=3)
178
@patch('bulk_email.models.html_to_text', Mock(return_value='Mocking CourseEmail.text_message', autospec=True))
179
class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCase):
180 181 182
    """Tests API methods that involve the submission of course-based background tasks."""

    def setUp(self):
183 184
        super(InstructorTaskCourseSubmitTest, self).setUp()

185 186 187 188 189
        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):
190
        """Create CourseEmail object for testing."""
191 192 193 194 195 196 197
        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>"
        )
198
        return course_email.id
199

200 201 202 203 204 205 206 207 208
    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()
209
        instructor_task = InstructorTask.objects.get(id=instructor_task.id)
210 211 212
        instructor_task.task_state = PROGRESS
        instructor_task.save()
        with self.assertRaises(AlreadyRunningError):
213 214 215 216 217 218 219 220 221 222 223
            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)

224 225 226 227 228 229 230 231
    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)

232 233 234 235 236 237 238
    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)
239

240 241 242 243 244
    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
245 246 247 248 249 250
    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)

251 252 253 254 255 256
    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)

257 258 259 260 261 262 263 264
    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)

265 266 267 268 269 270 271
    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)
272

273 274 275 276 277 278 279 280 281 282
    def test_submit_ora2_request_task(self):
        request = self.create_task_request(self.instructor)

        with patch('instructor_task.api.submit_task') as mock_submit_task:
            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, {}, '')

283 284 285 286
    def test_submit_generate_certs_students(self):
        """
        Tests certificates generation task submission api
        """
287
        api_call = lambda: generate_certificates_for_students(
288 289 290 291
            self.create_task_request(self.instructor),
            self.course.id
        )
        self._test_resubmission(api_call)
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306

    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)
307

308 309 310 311 312 313 314 315 316 317 318 319
    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
            )

320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
    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())