Ejemplo n.º 1
0
    def test_course_published(self):
        """
        Verify that publisher user can publish course run.
        """
        # Needs to be a backing course metadata object for publish to work
        discovery_run = DiscoveryCourseRunFactory(key=self.course_run.lms_course_id,
                                                  status=DiscoveryCourseRunStatus.Unpublished,
                                                  announcement=None)

        course = self.course_run.course
        self.run_state.name = CourseRunStateChoices.Approved
        self.run_state.preview_accepted = True
        self.run_state.save()

        self._assign_role(course, PublisherUserRole.Publisher, self.user)
        self._assign_role(course, PublisherUserRole.CourseTeam, UserFactory())

        response = self.client.patch(
            self.change_state_url,
            data=json.dumps({'name': CourseRunStateChoices.Published}),
            content_type=JSON_CONTENT_TYPE
        )

        self.assertEqual(response.status_code, 200)

        discovery_run.refresh_from_db()
        self.assertIsNotNone(discovery_run.announcement)
        self.assertEqual(discovery_run.status, DiscoveryCourseRunStatus.Published)

        self.run_state = CourseRunState.objects.get(course_run=self.course_run)
        self.assertTrue(self.run_state.is_published)

        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual([course.course_team_admin.email], mail.outbox[0].to)

        course_key = CourseKey.from_string(self.course_run.lms_course_id)
        expected_subject = 'Publication complete: About page for {course_name} {run_number}'.format(
            course_name=course.title,
            run_number=course_key.run
        )
        self.assertEqual(str(mail.outbox[0].subject), expected_subject)
        self.assertIn('has been published', mail.outbox[0].body.strip())
Ejemplo n.º 2
0
    def test_published(self):
        person = PersonFactory()
        org = OrganizationFactory()
        primary = DiscoveryCourseRunFactory(key=self.course_run.lms_course_id, staff=[person],
                                            status=CourseRunStatus.Unpublished, announcement=None,
                                            course__partner=self.partner, end=None, enrollment_end=None)
        second = DiscoveryCourseRunFactory(course=primary.course, status=CourseRunStatus.Published, end=None,
                                           enrollment_end=None, start=(primary.start + datetime.timedelta(days=1)))
        third = DiscoveryCourseRunFactory(course=primary.course, status=CourseRunStatus.Published,
                                          end=datetime.datetime(2010, 1, 1, tzinfo=UTC), enrollment_end=None)
        primary.course.authoring_organizations.add(org)
        self.course.organizations.add(org)
        ensure_draft_world(DiscoveryCourse.objects.get(pk=primary.course.pk))

        pc = UserFactory()
        factories.CourseUserRoleFactory(course=self.course, role=PublisherUserRole.ProjectCoordinator, user=pc)
        factories.OrganizationUserRoleFactory(organization=org, role=InternalUserRole.ProjectCoordinator, user=pc)

        self.mock_api_client()

        lookup_value = getattr(primary, self.publisher.unique_field)
        self.mock_node_retrieval(self.publisher.node_lookup_field, lookup_value)
        lookup_value = getattr(third, self.publisher.unique_field)
        self.mock_node_retrieval(self.publisher.node_lookup_field, lookup_value)

        self.mock_get_redirect_form()
        self.mock_add_redirect()

        self.course_run.course_run_state.name = CourseRunStateChoices.Approved
        self.course_run.course_run_state.change_state(CourseRunStateChoices.Published, self.user, self.site)
        primary.refresh_from_db()
        second.refresh_from_db()
        third.refresh_from_db()

        self.assertIsNotNone(primary.announcement)
        self.assertEqual(primary.status, CourseRunStatus.Published)
        self.assertEqual(second.status, CourseRunStatus.Published)  # doesn't change end=None runs
        self.assertEqual(third.status, CourseRunStatus.Unpublished)  # does change archived runs

        # Check email was sent (only one - from old publisher, not new publisher flow)
        assert len(mail.outbox) == 1
        message = mail.outbox[0]
        self.assertTrue(message.subject.startswith('Publication complete: '))
        self.assertEqual(message.to, [self.user.email])
        self.assertEqual(message.cc, [pc.email])
Ejemplo n.º 3
0
class CourseRunViewSetTests(SerializationMixin, ElasticsearchTestMixin,
                            OAuth2Mixin, APITestCase):
    def setUp(self):
        super(CourseRunViewSetTests, self).setUp()
        self.user = UserFactory(is_staff=True)
        self.client.force_authenticate(self.user)
        self.course_run = CourseRunFactory(course__partner=self.partner)
        self.course_run.course.authoring_organizations.add(
            OrganizationFactory(key='course-id'))
        self.course_run_2 = CourseRunFactory(course__key='Test+Course',
                                             course__partner=self.partner)
        self.refresh_index()
        self.request = APIRequestFactory().get('/')
        self.request.user = self.user

    def mock_patch_to_studio(self, key, access_token=True, status=200):
        if access_token:
            self.mock_access_token()
        studio_url = '{root}/api/v1/course_runs/{key}/'.format(
            root=self.partner.studio_url.strip('/'), key=key)
        responses.add(responses.PATCH, studio_url, status=status)
        responses.add(responses.POST,
                      '{url}images/'.format(url=studio_url),
                      status=status)

    def mock_post_to_studio(self, key, access_token=True):
        if access_token:
            self.mock_access_token()
        studio_url = '{root}/api/v1/course_runs/'.format(
            root=self.partner.studio_url.strip('/'))
        responses.add(responses.POST, studio_url, status=200)
        responses.add(responses.POST,
                      '{url}{key}/images/'.format(url=studio_url, key=key),
                      status=200)

    def test_get(self):
        """ Verify the endpoint returns the details for a single course. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(11):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertEqual(response.data,
                         self.serialize_course_run(self.course_run))

    def test_get_exclude_deleted_programs(self):
        """ Verify the endpoint returns no associated deleted programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(12):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data.get('programs') == []

    def test_get_include_deleted_programs(self):
        """
        Verify the endpoint returns associated deleted programs
        with the 'include_deleted_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_deleted_programs=1'

        with self.assertNumQueries(14):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_deleted_programs': True})

    def test_get_exclude_unpublished_programs(self):
        """ Verify the endpoint returns no associated unpublished programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(12):
            response = self.client.get(url)
            assert response.status_code == 200
            assert response.data.get('programs') == []

    def test_get_include_unpublished_programs(self):
        """
        Verify the endpoint returns associated unpublished programs
        with the 'include_unpublished_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_unpublished_programs=1'

        with self.assertNumQueries(14):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_unpublished_programs': True})

    @responses.activate
    def test_create_minimum(self):
        """ Verify the endpoint supports creating a course_run with the least info. """
        course = self.course_run.course
        new_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+'))
        self.mock_post_to_studio(new_key)
        url = reverse('api:v1:course_run-list')

        # Send nothing - expect complaints
        response = self.client.post(url, {}, format='json')
        self.assertEqual(response.status_code, 400)
        self.assertDictEqual(
            response.data, {
                'course': ['This field is required.'],
                'start': ['This field is required.'],
                'end': ['This field is required.'],
            })

        # Send minimum requested
        response = self.client.post(url, {
            'course': course.key,
            'start': '2000-01-01T00:00:00Z',
            'end': '2001-01-01T00:00:00Z',
        },
                                    format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=new_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))
        self.assertEqual(new_course_run.pacing_type,
                         'instructor_paced')  # default we provide
        self.assertEqual(
            str(new_course_run.end),
            '2001-01-01 00:00:00+00:00')  # spot check that input made it

    @responses.activate
    def test_create_with_key(self):
        """ Verify the endpoint supports creating a course_run when specifying a key (if allowed). """
        course = self.course_run.course
        date_key = 'course-v1:{}+1T2000'.format(course.key.replace('/', '+'))
        desired_key = 'course-v1:{}+HowdyDoing'.format(
            course.key.replace('/', '+'))
        url = reverse('api:v1:course_run-list')

        data = {
            'course': course.key,
            'start': '2000-01-01T00:00:00Z',
            'end': '2001-01-01T00:00:00Z',
            'key': desired_key,
        }

        # If org doesn't specifically allow it, incoming key is ignored
        self.mock_post_to_studio(date_key)
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=date_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))

        # Turn on this feature for this org, notice that we can now specify the course key we want
        org_ext = OrganizationExtensionFactory(
            organization=course.authoring_organizations.first())
        org_ext.auto_create_in_studio = False  # badly named, but this controls whether we let org name their keys
        org_ext.save()
        self.mock_post_to_studio(desired_key, access_token=False)
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 201)
        new_course_run = CourseRun.objects.get(key=desired_key)
        self.assertDictEqual(response.data,
                             self.serialize_course_run(new_course_run))

    def test_create_if_in_org(self):
        """ Verify the endpoint supports creating a course_run with organization permissions. """
        url = reverse('api:v1:course_run-list')
        course = self.course_run.course
        data = {'course': course.key}

        self.user.is_staff = False
        self.user.save()

        # Not in org, not allowed to POST
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, 403)

        # Add to org
        org_ext = OrganizationExtensionFactory(
            organization=course.authoring_organizations.first())
        self.user.groups.add(org_ext.group)

        # now allowed to POST
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code,
                         400)  # missing start, but at least we got that far

    @responses.activate
    def test_partial_update(self):
        """ Verify the endpoint supports partially updating a course_run's fields, provided user has permission. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        expected_min_effort = 867
        expected_max_effort = 5309
        data = {
            'max_effort': expected_max_effort,
            'min_effort': expected_min_effort,
        }

        # Update this course_run with the new info
        response = self.client.patch(url, data, format='json')
        assert response.status_code == 200

        # refresh and make sure we have the new effort levels
        self.course_run.refresh_from_db()

        assert self.course_run.max_effort == expected_max_effort
        assert self.course_run.min_effort == expected_min_effort

    def test_partial_update_no_studio_url(self):
        """ Verify we skip pushing when no studio url is set. """
        self.partner.studio_url = None
        self.partner.save()

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with mock.patch(
                'course_discovery.apps.api.v1.views.course_runs.log.info'
        ) as mock_logger:
            response = self.client.patch(url, {}, format='json')

        self.assertEqual(response.status_code, 200)
        mock_logger.assert_called_with(
            'Not pushing course run info for %s to Studio as partner %s has no studio_url set.',
            self.course_run.key,
            self.partner.short_code,
        )

    def test_partial_update_bad_permission(self):
        """ Verify partially updating will fail if user doesn't have permission. """
        user = UserFactory(is_staff=False, is_superuser=False)
        self.client.force_authenticate(user)
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        response = self.client.patch(url, {}, format='json')
        assert response.status_code == 403

    @ddt.data(
        (
            {
                'start': '2010-01-01T00:00:00Z',
                'end': '2000-01-01T00:00:00Z'
            },
            'Start date cannot be after the End date',
        ),
        (
            {
                'key': 'course-v1:Blarg+Hello+Run'
            },
            'Key cannot be changed',
        ),
        (
            {
                'course': 'Test+Course'
            },
            'Course cannot be changed',
        ),
        (
            {
                'min_effort': 10000
            },
            'Minimum effort cannot be greater than Maximum effort',
        ),
        (
            {
                'min_effort': 10000,
                'max_effort': 10000
            },
            'Minimum effort and Maximum effort cannot be the same',
        ),
        (
            {
                'max_effort': None
            },
            'Maximum effort cannot be empty',
        ),
    )
    @ddt.unpack
    def test_partial_update_common_errors(self, data, error):
        """ Verify partially updating will fail depending on various validation checks. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, data, format='json')
        self.assertContains(response, error, status_code=400)

    def test_partial_update_staff(self):
        """ Verify partially updating allows staff updates. """
        self.mock_patch_to_studio(self.course_run.key)

        p1 = PersonFactory()
        p2 = PersonFactory()
        PersonFactory()

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, {'staff': [p2.uuid, p1.uuid]},
                                     format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertListEqual(list(self.course_run.staff.all()), [p2, p1])

    @responses.activate
    def test_partial_update_video(self):
        """ Verify partially updating allows video updates. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(
            url, {'video': {
                'src': 'https://example.com/blarg'
            }},
            format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.video.src,
                         'https://example.com/blarg')

    @responses.activate
    def test_update_if_editor(self):
        """ Verify the endpoint supports updating a course_run with editor permissions. """
        self.mock_patch_to_studio(self.course_run.key)
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        self.user.is_staff = False
        self.user.save()

        # Not an editor, not allowed to patch
        response = self.client.patch(url, {}, format='json')
        self.assertEqual(response.status_code, 403)

        # Add as editor
        org_ext = OrganizationExtensionFactory(
            organization=self.course_run.course.authoring_organizations.first(
            ))
        self.user.groups.add(org_ext.group)
        CourseEditorFactory(user=self.user, course=self.course_run.course)

        # now allowed to patch
        response = self.client.patch(url, {}, format='json')
        self.assertEqual(response.status_code, 200)

    @responses.activate
    def test_studio_update_failure(self):
        """ Verify we bubble up error correctly if studio is giving us static. """
        self.mock_patch_to_studio(self.course_run.key, status=400)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.patch(url, {'title': 'New Title'},
                                     format='json')
        self.assertContains(response,
                            'Failed to set course run data: Client Error 400',
                            status_code=400)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.title_override,
                         None)  # prove we didn't touch the course run object

    @responses.activate
    def test_full_update(self):
        """ Verify full updating is allowed. """
        self.mock_patch_to_studio(self.course_run.key)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
                'title': 'New Title',
            },
            format='json')
        self.assertEqual(response.status_code, 200)

        self.course_run.refresh_from_db()
        self.assertEqual(self.course_run.title_override, 'New Title')

    @ddt.data(
        CourseRunStatus.LegalReview,
        CourseRunStatus.InternalReview,
    )
    def test_patch_put_restrict_when_reviewing(self, status):
        self.course_run.status = status
        self.course_run.save()
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
            },
            format='json')
        assert response.status_code == 403

        response = self.client.patch(url, {}, format='json')
        assert response.status_code == 403

    @responses.activate
    def test_patch_put_reset_status(self):
        self.mock_patch_to_studio(self.course_run.key)
        self.course_run.status = CourseRunStatus.Reviewed
        self.course_run.save()
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.put(
            url,
            {
                'course':
                self.course_run.course.key,  # required, so we need for a put
                'start':
                self.course_run.start,  # required, so we need for a put
                'end': self.course_run.end,  # required, so we need for a put
                'status': 'reviewed',
            },
            format='json')
        assert response.status_code == 200
        self.course_run.refresh_from_db()
        assert self.course_run.status == CourseRunStatus.Unpublished

    def test_list(self):
        """ Verify the endpoint returns a list of all course runs. """
        url = reverse('api:v1:course_run-list')

        with self.assertNumQueries(13):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(CourseRun.objects.all().order_by(
                Lower('key')),
                                      many=True))

    def test_list_sorted_by_course_start_date(self):
        """ Verify the endpoint returns a list of all course runs sorted by start date. """
        url = '{root}?ordering=start'.format(
            root=reverse('api:v1:course_run-list'))

        with self.assertNumQueries(13):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(
                CourseRun.objects.all().order_by('start'), many=True))

    def test_list_query(self):
        """ Verify the endpoint returns a filtered list of courses """
        course_runs = CourseRunFactory.create_batch(
            3, title='Some random title', course__partner=self.partner)
        CourseRunFactory(title='non-matching name')
        query = 'title:Some random title'
        url = '{root}?q={query}'.format(root=reverse('api:v1:course_run-list'),
                                        query=query)

        with self.assertNumQueries(39):
            response = self.client.get(url)

        actual_sorted = sorted(response.data['results'],
                               key=lambda course_run: course_run['key'])
        expected_sorted = sorted(self.serialize_course_run(course_runs,
                                                           many=True),
                                 key=lambda course_run: course_run['key'])
        self.assertListEqual(actual_sorted, expected_sorted)

    def assert_list_results(self, url, expected, extra_context=None):
        expected = sorted(expected,
                          key=lambda course_run: course_run.key.lower())
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(expected,
                                      many=True,
                                      extra_context=extra_context))

    def test_filter_by_keys(self):
        """ Verify the endpoint returns a list of course runs filtered by the specified keys. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        keys = ','.join([course.key for course in expected])
        url = '{root}?keys={keys}'.format(
            root=reverse('api:v1:course_run-list'), keys=keys)
        self.assert_list_results(url, expected)

    def test_filter_by_marketable(self):
        """ Verify the endpoint filters course runs to those that are marketable. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        for course_run in expected:
            SeatFactory(course_run=course_run)

        CourseRunFactory.create_batch(3,
                                      slug=None,
                                      course__partner=self.partner)
        CourseRunFactory.create_batch(3, slug='', course__partner=self.partner)

        url = reverse('api:v1:course_run-list') + '?marketable=1'
        self.assert_list_results(url, expected)

    def test_filter_by_hidden(self):
        """ Verify the endpoint filters course runs that are hidden. """
        CourseRun.objects.all().delete()
        course_runs = CourseRunFactory.create_batch(
            3, course__partner=self.partner)
        hidden_course_runs = CourseRunFactory.create_batch(
            3, hidden=True, course__partner=self.partner)
        url = reverse('api:v1:course_run-list')
        self.assert_list_results(url, course_runs + hidden_course_runs)
        url = reverse('api:v1:course_run-list') + '?hidden=False'
        self.assert_list_results(url, course_runs)

    def test_filter_by_active(self):
        """ Verify the endpoint filters course runs to those that are active. """
        CourseRun.objects.all().delete()

        # Create course with end date in future and enrollment_end in past.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) - datetime.timedelta(days=1)
        CourseRunFactory(end=end,
                         enrollment_end=enrollment_end,
                         course__partner=self.partner)

        # Create course with end date in past and no enrollment_end.
        end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
        CourseRunFactory(end=end,
                         enrollment_end=None,
                         course__partner=self.partner)

        # Create course with end date in future and enrollment_end in future.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) + datetime.timedelta(days=1)
        active_enrollment_end = CourseRunFactory(end=end,
                                                 enrollment_end=enrollment_end,
                                                 course__partner=self.partner)

        # Create course with end date in future and no enrollment_end.
        active_no_enrollment_end = CourseRunFactory(
            end=end, enrollment_end=None, course__partner=self.partner)

        expected = [active_enrollment_end, active_no_enrollment_end]
        url = reverse('api:v1:course_run-list') + '?active=1'
        self.assert_list_results(url, expected)

    def test_filter_by_license(self):
        CourseRun.objects.all().delete()
        course_runs_cc = CourseRunFactory.create_batch(
            3, course__partner=self.partner, license='cc-by-sa')
        CourseRunFactory.create_batch(3,
                                      course__partner=self.partner,
                                      license='')

        url = reverse('api:v1:course_run-list') + '?license=cc-by-sa'
        self.assert_list_results(url, course_runs_cc)

    def test_list_exclude_utm(self):
        """ Verify the endpoint returns marketing URLs without UTM parameters. """
        url = reverse('api:v1:course_run-list') + '?exclude_utm=1'
        self.assert_list_results(url,
                                 CourseRun.objects.all(),
                                 extra_context={'exclude_utm': 1})

    def test_contains_single_course_run(self):
        """ Verify that a single course_run is contained in a query """
        qs = urllib.parse.urlencode({
            'query': 'id:course*',
            'course_run_ids': self.course_run.key,
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertEqual(response.data,
                         {'course_runs': {
                             self.course_run.key: True
                         }})

    def test_contains_multiple_course_runs(self):
        qs = urllib.parse.urlencode({
            'query':
            'id:course*',
            'course_run_ids':
            '{},{},{}'.format(self.course_run.key, self.course_run_2.key,
                              'abc')
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 200
        self.assertDictEqual(
            response.data, {
                'course_runs': {
                    self.course_run.key: True,
                    self.course_run_2.key: True,
                    'abc': False
                }
            })

    @ddt.data({'params': {
        'course_run_ids': 'a/b/c'
    }}, {'params': {
        'query': 'id:course*'
    }}, {'params': {}})
    @ddt.unpack
    def test_contains_missing_parameter(self, params):
        qs = urllib.parse.urlencode(params)
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 400

    def test_options(self):
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        response = self.client.options(url)
        self.assertEqual(response.status_code, 200)

        data = response.data['actions']['PUT']
        self.assertEqual(data['level_type']['choices'],
                         [{
                             'display_name': self.course_run.level_type.name,
                             'value': self.course_run.level_type.name
                         }, {
                             'display_name': self.course_run_2.level_type.name,
                             'value': self.course_run_2.level_type.name
                         }])
        self.assertEqual(data['content_language']['choices'],
                         [{
                             'display_name': x.name,
                             'value': x.code
                         } for x in LanguageTag.objects.all()])
        self.assertTrue(LanguageTag.objects.count() > 0)
Ejemplo n.º 4
0
class CourseRunViewSetTests(SerializationMixin, ElasticsearchTestMixin,
                            APITestCase):
    def setUp(self):
        super(CourseRunViewSetTests, self).setUp()
        self.user = UserFactory(is_staff=True, is_superuser=True)
        self.client.force_authenticate(self.user)
        self.course_run = CourseRunFactory(course__partner=self.partner)
        self.course_run_2 = CourseRunFactory(course__partner=self.partner)
        self.refresh_index()
        self.request = APIRequestFactory().get('/')
        self.request.user = self.user

    def test_get(self):
        """ Verify the endpoint returns the details for a single course. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(10):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertEqual(response.data,
                         self.serialize_course_run(self.course_run))

    def test_get_exclude_deleted_programs(self):
        """ Verify the endpoint returns no associated deleted programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(11):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data.get('programs') == []

    def test_get_include_deleted_programs(self):
        """
        Verify the endpoint returns associated deleted programs
        with the 'include_deleted_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Deleted)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_deleted_programs=1'

        with self.assertNumQueries(13):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_deleted_programs': True})

    def test_get_exclude_unpublished_programs(self):
        """ Verify the endpoint returns no associated unpublished programs """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        with self.assertNumQueries(11):
            response = self.client.get(url)
            assert response.status_code == 200
            assert response.data.get('programs') == []

    def test_get_include_unpublished_programs(self):
        """
        Verify the endpoint returns associated unpublished programs
        with the 'include_unpublished_programs' flag set to True
        """
        ProgramFactory(courses=[self.course_run.course],
                       status=ProgramStatus.Unpublished)

        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})
        url += '?include_unpublished_programs=1'

        with self.assertNumQueries(13):
            response = self.client.get(url)
        assert response.status_code == 200
        assert response.data == \
            self.serialize_course_run(self.course_run, extra_context={'include_unpublished_programs': True})

    def test_partial_update(self):
        """ Verify the endpoint supports partially updating a course_run's fields, provided user has permission. """
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        expected_min_effort = 867
        expected_max_effort = 5309
        data = {
            'max_effort': expected_max_effort,
            'min_effort': expected_min_effort,
        }

        # Update this course_run with the new info
        response = self.client.patch(url, data, format='json')
        assert response.status_code == 200

        # refresh and make sure we have the new effort levels
        self.course_run.refresh_from_db()

        assert self.course_run.max_effort == expected_max_effort
        assert self.course_run.min_effort == expected_min_effort

    def test_partial_update_bad_permission(self):
        """ Verify partially updating will fail if user doesn't have permission. """
        user = UserFactory(is_staff=False, is_superuser=False)
        self.client.force_authenticate(user)
        url = reverse('api:v1:course_run-detail',
                      kwargs={'key': self.course_run.key})

        response = self.client.patch(url, {}, format='json')
        assert response.status_code == 403

    def test_list(self):
        """ Verify the endpoint returns a list of all course runs. """
        url = reverse('api:v1:course_run-list')

        with self.assertNumQueries(11):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(CourseRun.objects.all().order_by(
                Lower('key')),
                                      many=True))

    def test_list_sorted_by_course_start_date(self):
        """ Verify the endpoint returns a list of all course runs sorted by start date. """
        url = '{root}?ordering=start'.format(
            root=reverse('api:v1:course_run-list'))

        with self.assertNumQueries(11):
            response = self.client.get(url)

        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(
                CourseRun.objects.all().order_by('start'), many=True))

    def test_list_query(self):
        """ Verify the endpoint returns a filtered list of courses """
        course_runs = CourseRunFactory.create_batch(
            3, title='Some random title', course__partner=self.partner)
        CourseRunFactory(title='non-matching name')
        query = 'title:Some random title'
        url = '{root}?q={query}'.format(root=reverse('api:v1:course_run-list'),
                                        query=query)

        with self.assertNumQueries(36):
            response = self.client.get(url)

        actual_sorted = sorted(response.data['results'],
                               key=lambda course_run: course_run['key'])
        expected_sorted = sorted(self.serialize_course_run(course_runs,
                                                           many=True),
                                 key=lambda course_run: course_run['key'])
        self.assertListEqual(actual_sorted, expected_sorted)

    def assert_list_results(self, url, expected, extra_context=None):
        expected = sorted(expected,
                          key=lambda course_run: course_run.key.lower())
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertListEqual(
            response.data['results'],
            self.serialize_course_run(expected,
                                      many=True,
                                      extra_context=extra_context))

    def test_filter_by_keys(self):
        """ Verify the endpoint returns a list of course runs filtered by the specified keys. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        keys = ','.join([course.key for course in expected])
        url = '{root}?keys={keys}'.format(
            root=reverse('api:v1:course_run-list'), keys=keys)
        self.assert_list_results(url, expected)

    def test_filter_by_marketable(self):
        """ Verify the endpoint filters course runs to those that are marketable. """
        CourseRun.objects.all().delete()
        expected = CourseRunFactory.create_batch(3,
                                                 course__partner=self.partner)
        for course_run in expected:
            SeatFactory(course_run=course_run)

        CourseRunFactory.create_batch(3,
                                      slug=None,
                                      course__partner=self.partner)
        CourseRunFactory.create_batch(3, slug='', course__partner=self.partner)

        url = reverse('api:v1:course_run-list') + '?marketable=1'
        self.assert_list_results(url, expected)

    def test_filter_by_hidden(self):
        """ Verify the endpoint filters course runs that are hidden. """
        CourseRun.objects.all().delete()
        course_runs = CourseRunFactory.create_batch(
            3, course__partner=self.partner)
        hidden_course_runs = CourseRunFactory.create_batch(
            3, hidden=True, course__partner=self.partner)
        url = reverse('api:v1:course_run-list')
        self.assert_list_results(url, course_runs + hidden_course_runs)
        url = reverse('api:v1:course_run-list') + '?hidden=False'
        self.assert_list_results(url, course_runs)

    def test_filter_by_active(self):
        """ Verify the endpoint filters course runs to those that are active. """
        CourseRun.objects.all().delete()

        # Create course with end date in future and enrollment_end in past.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) - datetime.timedelta(days=1)
        CourseRunFactory(end=end,
                         enrollment_end=enrollment_end,
                         course__partner=self.partner)

        # Create course with end date in past and no enrollment_end.
        end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=2)
        CourseRunFactory(end=end,
                         enrollment_end=None,
                         course__partner=self.partner)

        # Create course with end date in future and enrollment_end in future.
        end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=2)
        enrollment_end = datetime.datetime.now(
            pytz.UTC) + datetime.timedelta(days=1)
        active_enrollment_end = CourseRunFactory(end=end,
                                                 enrollment_end=enrollment_end,
                                                 course__partner=self.partner)

        # Create course with end date in future and no enrollment_end.
        active_no_enrollment_end = CourseRunFactory(
            end=end, enrollment_end=None, course__partner=self.partner)

        expected = [active_enrollment_end, active_no_enrollment_end]
        url = reverse('api:v1:course_run-list') + '?active=1'
        self.assert_list_results(url, expected)

    def test_filter_by_license(self):
        CourseRun.objects.all().delete()
        course_runs_cc = CourseRunFactory.create_batch(
            3, course__partner=self.partner, license='cc-by-sa')
        CourseRunFactory.create_batch(3,
                                      course__partner=self.partner,
                                      license='')

        url = reverse('api:v1:course_run-list') + '?license=cc-by-sa'
        self.assert_list_results(url, course_runs_cc)

    def test_list_exclude_utm(self):
        """ Verify the endpoint returns marketing URLs without UTM parameters. """
        url = reverse('api:v1:course_run-list') + '?exclude_utm=1'
        self.assert_list_results(url,
                                 CourseRun.objects.all(),
                                 extra_context={'exclude_utm': 1})

    def test_contains_single_course_run(self):
        """ Verify that a single course_run is contained in a query """
        qs = urllib.parse.urlencode({
            'query': 'id:course*',
            'course_run_ids': self.course_run.key,
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)
        response = self.client.get(url)
        assert response.status_code == 200
        self.assertEqual(response.data,
                         {'course_runs': {
                             self.course_run.key: True
                         }})

    def test_contains_multiple_course_runs(self):
        qs = urllib.parse.urlencode({
            'query':
            'id:course*',
            'course_run_ids':
            '{},{},{}'.format(self.course_run.key, self.course_run_2.key,
                              'abc')
        })
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 200
        self.assertDictEqual(
            response.data, {
                'course_runs': {
                    self.course_run.key: True,
                    self.course_run_2.key: True,
                    'abc': False
                }
            })

    @ddt.data({'params': {
        'course_run_ids': 'a/b/c'
    }}, {'params': {
        'query': 'id:course*'
    }}, {'params': {}})
    @ddt.unpack
    def test_contains_missing_parameter(self, params):
        qs = urllib.parse.urlencode(params)
        url = '{}?{}'.format(reverse('api:v1:course_run-contains'), qs)

        response = self.client.get(url)
        assert response.status_code == 400
Ejemplo n.º 5
0
class TestMigrateCommentsToSalesforce(TestCase):
    LOGGER_PATH = 'course_discovery.apps.course_metadata.management.commands.migrate_comments_to_salesforce.logger'

    def setUp(self):
        super(TestMigrateCommentsToSalesforce, self).setUp()
        self.partner = PartnerFactory()
        self.user_1 = UserFactory()
        self.org_1 = OrganizationFactory(partner=self.partner)
        self.course_1 = CourseFactory(
            partner=self.partner,
            authoring_organizations=[self.org_1],
            key=self.org_1.key + '+101x',
            title='Old Title',
        )
        self.course_run_1 = CourseRunFactory(
            key='course-v1:{key}+1T2019'.format(
                key=self.course_1.key,
            ),
            course=self.course_1,
        )

        self.publisher_course_1 = PublisherCourseFactory(number='101x', title='New Title')
        self.publisher_course_1.organizations.add(self.org_1)  # pylint: disable=no-member
        self.publisher_course_run_1 = PublisherCourseRunFactory(
            course=self.publisher_course_1,
            lms_course_id='course-v1:{org}+{number}+1T2019'.format(
                org=self.org_1.key, number=self.publisher_course_1.number
            ),
        )

    def tearDown(self):
        super(TestMigrateCommentsToSalesforce, self).tearDown()
        # Zero out the instances that are created during testing
        SalesforceUtil.instances = {}

    @mock.patch(LOGGER_PATH)
    def test_handle_no_orgs(self, mock_logger):
        config = MigrateCommentsToSalesforceFactory()
        config.orgs.all().delete()

        with self.assertRaises(CommandError):
            Command().handle()
        mock_logger.error.assert_called_with(
            'No organizations were defined. Please add organizations to the MigrateCommentsToSalesforce model.'
        )

    @mock.patch(LOGGER_PATH)
    def test_handle_no_partner(self, mock_logger):
        config = MigrateCommentsToSalesforceFactory()
        config.orgs.add(self.org_1)
        with self.assertRaises(CommandError):
            Command().handle()
        mock_logger.error.assert_called_with(
            'No partner was defined. Please add a partner to the MigrateCommentsToSalesforce model.'
        )

    @mock.patch(LOGGER_PATH)
    def test_handle_no_salesforce_configuration(self, mock_logger):
        config = MigrateCommentsToSalesforceFactory(partner=self.partner)
        config.orgs.add(self.org_1)
        with self.assertRaises(CommandError):
            Command().handle()
        mock_logger.error.assert_called_with(
            'Salesforce configuration for {} does not exist'.format(self.partner.name)
        )

    @mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce')
    def test_handle_without_publisher_course_run(self, mock_salesforce):
        config = MigrateCommentsToSalesforceFactory(partner=self.partner)
        config.orgs.add(self.org_1)
        SalesforceConfigurationFactory(partner=self.partner)

        self.publisher_course_run_1.delete()

        # Set return values for all of the Salesforce methods that get called
        mock_salesforce().Publisher_Organization__c.create.return_value = {'id': 'SomePubOrgId'}
        mock_salesforce().Course__c.create.return_value = {'id': 'SomeCourseId'}
        mock_salesforce().Case.create.return_value = {'id': 'SomeCaseId'}
        mock_salesforce().Course_Run__c.create.return_value = {'id': 'SomeCourseRunId'}

        with mock.patch(self.LOGGER_PATH) as mock_logger:
            Command().handle()
            calls = [
                mock.call('No PublisherCourseRun found for {}.'.format(self.course_run_1.key)),
                mock.call('No PublisherCourses found for {}'.format(self.course_1.key))
            ]
            mock_logger.warning.assert_has_calls(calls, any_order=True)

    @mock.patch('course_discovery.apps.course_metadata.salesforce.Salesforce')
    def test_handle_with_comments(self, mock_salesforce):
        config = MigrateCommentsToSalesforceFactory(partner=self.partner)
        config.orgs.add(self.org_1)
        SalesforceConfigurationFactory(partner=self.partner)
        course_comment = CommentFactory(
            user=self.user_1,
            content_type_id=ContentType.objects.get_for_model(PublisherCourse),
            object_pk=self.publisher_course_1.id,
        )
        course_comment.content_type_id = ContentType.objects.get_for_model(PublisherCourse)
        course_comment.object_pk = self.publisher_course_1.id
        course_comment.save()

        course_run_comment = CommentFactory(
            user=self.user_1,
            content_type_id=ContentType.objects.get_for_model(PublisherCourseRun),
            object_pk=self.publisher_course_run_1.id,
        )
        course_run_comment.content_type_id = ContentType.objects.get_for_model(PublisherCourseRun)
        course_run_comment.object_pk = self.publisher_course_run_1.id
        course_run_comment.save()

        # Set return values for all of the Salesforce methods that get called
        mock_salesforce().Publisher_Organization__c.create.return_value = {'id': 'SomePubOrgId'}
        mock_salesforce().Course__c.create.return_value = {'id': 'SomeCourseId'}
        mock_salesforce().Case.create.return_value = {'id': 'SomeCaseId'}
        mock_salesforce().Course_Run__c.create.return_value = {'id': 'SomeCourseRunId'}

        with mock.patch(self.LOGGER_PATH) as mock_logger:
            Command().handle()
            self.org_1.refresh_from_db()
            self.course_1.refresh_from_db()
            self.course_run_1.refresh_from_db()

            self.assertEqual(self.org_1.salesforce_id, 'SomePubOrgId')
            self.assertEqual(self.course_1.salesforce_id, 'SomeCourseId')
            self.assertEqual(self.course_1.salesforce_case_id, 'SomeCaseId')
            self.assertEqual(self.course_run_1.salesforce_id, 'SomeCourseRunId')

            mock_logger.info.assert_called_with('Inserted 2 comments for {}'.format(self.course_1.title))