Ejemplo n.º 1
0
    def _define_course_metadata(self):
        # Add course runs, courses, and programs to DB to copy data into
        courses = {}
        # Course runs map to courses in the way opaque keys would (without actually using opaque keys code)
        course_to_run_mapping = {
            '00test/00test/00test': '00test/00test',
            '00test/00test/01test': '00test/00test',
            '00test/01test/00test': '00test/01test',
            '00test/01test/01test': '00test/01test',
            '00test/01test/02test': '00test/01test',
            '00test/02test/00test': '00test/02test'
        }
        for course_summary in self.mocked_data:
            course_run_key = course_summary['course_id']
            course_key = course_to_run_mapping[course_run_key]
            if course_key in courses:
                course = courses[course_key]
                course_run = CourseRunFactory(key=course_summary['course_id'],
                                              course=course)
                course_run.save()
            else:
                course = CourseFactory(key=course_key)
                course.save()
                course_run = CourseRunFactory(key=course_summary['course_id'],
                                              course=course)
                course_run.save()
                courses[course_key] = course_run.course

        # Create a program with all of the courses we created
        program = ProgramFactory()
        program.courses.set(courses.values())  # pylint: disable=no-member
Ejemplo n.º 2
0
    def test_available(self):
        """
        Verify the method filters Courses to those which contain at least one
        CourseRun that can be enrolled in immediately, is ongoing or yet to start,
        and appears on the marketing site.
        """
        for state in self.states():
            Course.objects.all().delete()

            course_run = CourseRunFactory()
            for function in state:
                function(course_run)

            course_run.save()

            if state in self.available_states:
                course = course_run.course
                # This course is available, so should be returned by the
                # available() query.
                assert list(Course.objects.available()) == [course]

                # This run has no seats, but we still expect its parent course
                # to be included.
                CourseRunFactory(course=course)
                assert list(Course.objects.available()) == [course]

                # Generate another course run with available seats.
                # Only one instance of the course should be included in the result.
                other_course_run = CourseRunFactory(course=course)
                for function in state:
                    function(other_course_run)
                other_course_run.save()
                assert list(Course.objects.available()) == [course]
            else:
                assert list(Course.objects.available()) == []
Ejemplo n.º 3
0
    def test_populate_official_with_existing_draft(self):
        course_run = CourseRunFactory(draft=True,
                                      course=CourseFactory(draft=True))
        course_run.status = CourseRunStatus.Reviewed
        course_run.save()

        salesforce_id = 'SomeSalesforceId'

        course_run_2 = CourseRunFactory(draft=True,
                                        course=CourseFactory(
                                            draft=True,
                                            salesforce_id=salesforce_id),
                                        salesforce_id=salesforce_id)
        course_run_2.status = CourseRunStatus.Reviewed
        course_run_2.save()

        # Need to modify state of the instance passed in
        def new_create_instance(instance):
            instance.salesforce_id = salesforce_id
            instance.save()

        with mock.patch(
                'course_discovery.apps.course_metadata.tests.test_salesforce.CourseRun.save'
        ):
            with mock.patch(
                    'course_discovery.apps.course_metadata.tests.test_salesforce.Course.save'
            ):
                with mock.patch(
                        self.salesforce_util_path) as mock_salesforce_util:
                    with mock.patch(self.salesforce_util_path +
                                    '.create_course_run',
                                    new=new_create_instance):
                        with mock.patch(self.salesforce_util_path +
                                        '.create_course',
                                        new=new_create_instance):
                            created = populate_official_with_existing_draft(
                                course_run.official_version,
                                mock_salesforce_util)
                            assert created
                            assert course_run.official_version.salesforce_id == salesforce_id

                            created = populate_official_with_existing_draft(
                                course_run.official_version.course,
                                mock_salesforce_util)
                            assert created
                            assert course_run.official_version.course.salesforce_id == salesforce_id

                            created = populate_official_with_existing_draft(
                                course_run_2.official_version,
                                mock_salesforce_util)
                            assert not created
                            assert course_run_2.official_version.salesforce_id == salesforce_id

                            created = populate_official_with_existing_draft(
                                course_run_2.official_version.course,
                                mock_salesforce_util)
                            assert not created
                            assert course_run_2.official_version.course.salesforce_id == salesforce_id
Ejemplo n.º 4
0
    def test_marketable_exclusions(self):
        """ Verify the method excludes CourseRuns without a slug. """
        course_run = CourseRunFactory()
        SeatFactory(course_run=course_run)

        course_run.slug = ''  # blank out auto-generated slug
        course_run.save()

        self.assertEqual(CourseRun.objects.marketable().exists(), False)
Ejemplo n.º 5
0
    def test_marketable_unpublished_exclusions(self, is_published):
        """ Verify the method excludes CourseRuns with Unpublished status. """
        course_run = CourseRunFactory(status=CourseRunStatus.Unpublished)
        SeatFactory(course_run=course_run)

        if is_published:
            course_run.status = CourseRunStatus.Published
            course_run.save()

        assert CourseRun.objects.marketable().exists() == is_published
    def test_ignores_drafts(self, mock_publish):
        # Draft run doesn't get published
        run = CourseRunFactory(draft=True, status=CourseRunStatus.Reviewed, go_live_date=self.past)
        self.handle()
        self.assertEqual(mock_publish.call_count, 0)

        # But sanity check by confirming that if it *is* an official version, it does.
        run.draft = False
        run.save()
        self.handle()
        self.assertEqual(mock_publish.call_count, 1)
Ejemplo n.º 7
0
    def create_discovery_course_run_with_metadata(course, metadata):
        """
        Creates and returns a Discovery CourseRun object with course and fields specified in metadata dictionary.

        Arguments:
            course: a Course object to assign to the created Discovery CourseRun.course field
            metadata: a dictionary where the keys are field names and values are field values

            For example, metadata could be {'pacing_type': 'Instructor-paced'}.

        Returns:
            a Discovery CourseRun object
        """
        discovery_course_run = DiscoveryCourseRunFactory(course=course, **metadata)
        discovery_course_run.save()
        return discovery_course_run
    def test_courses(self):
        """
        Verify the endpoint returns the list of available courses contained in
        the catalog, and that courses appearing in the response always have at
        least one serialized run.
        """
        url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id})

        for state in self.states():
            Course.objects.all().delete()

            course_run = CourseRunFactory(course__title='ABC Test Course')
            for function in state:
                function(course_run)

            course_run.save()

            if state in self.available_states:
                course = course_run.course

                # This run has no seats, but we still expect its parent course
                # to be included.
                filtered_course_run = CourseRunFactory(course=course)

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

                assert response.status_code == 200

                # Emulate prefetching behavior.
                filtered_course_run.delete()

                assert response.data[
                    'results'] == self.serialize_catalog_course([course],
                                                                many=True)

                # Any course appearing in the response must have at least one serialized run.
                assert len(response.data['results'][0]['course_runs']) > 0
            else:
                with self.assertNumQueries(3):
                    response = self.client.get(url)

                assert response.status_code == 200
                assert response.data['results'] == []
class AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin, APITestCase):
    """ Tests for the AffiliateWindowViewSet. """

    def setUp(self):
        super(AffiliateWindowViewSetTests, self).setUp()
        self.user = UserFactory()
        self.client.force_authenticate(self.user)
        self.catalog = CatalogFactory(query='*:*', viewers=[self.user])

        self.enrollment_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30)
        self.course_end = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=60)
        self.course_run = CourseRunFactory(enrollment_end=self.enrollment_end, end=self.course_end)

        self.seat_verified = SeatFactory(course_run=self.course_run, type=Seat.VERIFIED)
        self.course = self.course_run.course
        self.affiliate_url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': self.catalog.id})
        self.refresh_index()

    def test_without_authentication(self):
        """ Verify authentication is required when accessing the endpoint. """
        self.client.logout()
        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 403)

    def test_affiliate_with_supported_seats(self):
        """ Verify that endpoint returns course runs for verified and professional seats only. """
        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(1, len(root.findall('product')))
        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0],
            self.seat_verified
        )

        # Add professional seat.
        seat_professional = SeatFactory(course_run=self.course_run, type=Seat.PROFESSIONAL)

        response = self.client.get(self.affiliate_url)
        root = ET.fromstring(response.content)
        self.assertEqual(2, len(root.findall('product')))

        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, self.seat_verified.type))[0],
            self.seat_verified
        )
        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(self.course_run.key, seat_professional.type))[0],
            seat_professional
        )

    @ddt.data(Seat.CREDIT, Seat.HONOR, Seat.AUDIT)
    def test_with_non_supported_seats(self, non_supporting_seat):
        """ Verify that endpoint returns no data for honor, credit and audit seats. """

        self.seat_verified.type = non_supporting_seat
        self.seat_verified.save()

        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(0, len(root.findall('product')))

    def test_with_closed_enrollment(self):
        """ Verify that endpoint returns no data if enrollment is close. """
        self.course_run.enrollment_end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=100)
        self.course_run.end = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=100)
        self.course_run.save()

        # new course run with future end date and no enrollment_date.
        CourseRunFactory(end=self.course_end, course=self.course, enrollment_end=None)

        response = self.client.get(self.affiliate_url)

        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(0, len(root.findall('product')))

    def assert_product_xml(self, content, seat):
        """ Helper method to verify product data in xml format. """
        self.assertEqual(content.find('pid').text, '{}-{}'.format(self.course_run.key, seat.type))
        self.assertEqual(content.find('name').text, self.course_run.title)
        self.assertEqual(content.find('desc').text, self.course_run.short_description)
        self.assertEqual(content.find('purl').text, self.course_run.marketing_url)
        self.assertEqual(content.find('imgurl').text, self.course_run.image.src)
        self.assertEqual(content.find('price/actualp').text, str(seat.price))
        self.assertEqual(content.find('currency').text, seat.currency.code)
        self.assertEqual(content.find('category').text, AffiliateWindowSerializer.CATEGORY)

    def test_dtd_with_valid_data(self):
        """ Verify the XML data produced by the endpoint conforms to the DTD file. """
        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 200)
        filename = abspath(join(dirname(dirname(__file__)), 'affiliate_window_product_feed.1.4.dtd'))
        dtd = etree.DTD(open(filename))

        root = etree.XML(response.content)
        self.assertTrue(dtd.validate(root))

    def test_permissions(self):
        """ Verify only users with the appropriate permissions can access the endpoint. """
        catalog = CatalogFactory()
        superuser = UserFactory(is_superuser=True)
        url = reverse('api:v1:partners:affiliate_window-detail', kwargs={'pk': catalog.id})

        # Superusers can view all catalogs
        self.client.force_authenticate(superuser)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        # Regular users can only view catalogs belonging to them
        self.client.force_authenticate(self.user)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 403)

        catalog.viewers = [self.user]
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
Ejemplo n.º 10
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.º 11
0
class AffiliateWindowViewSetTests(ElasticsearchTestMixin, SerializationMixin,
                                  APITestCase):
    """ Tests for the AffiliateWindowViewSet. """
    def setUp(self):
        super(AffiliateWindowViewSetTests, self).setUp()
        self.user = UserFactory()
        self.client.force_authenticate(self.user)
        self.catalog = CatalogFactory(query='*:*', viewers=[self.user])

        self.enrollment_end = datetime.datetime.now(
            pytz.UTC) + datetime.timedelta(days=30)
        self.course_end = datetime.datetime.now(
            pytz.UTC) + datetime.timedelta(days=60)
        self.course_run = CourseRunFactory(enrollment_end=self.enrollment_end,
                                           end=self.course_end)

        self.seat_verified = SeatFactory(course_run=self.course_run,
                                         type=SeatTypeFactory.verified())
        self.course = self.course_run.course
        self.affiliate_url = reverse('api:v1:partners:affiliate_window-detail',
                                     kwargs={'pk': self.catalog.id})
        self.refresh_index()

    def test_without_authentication(self):
        """ Verify authentication is required when accessing the endpoint. """
        self.client.logout()
        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 401)

    def test_affiliate_with_supported_seats(self):
        """ Verify that endpoint returns course runs for verified and professional seats only. """
        response = self.client.get(self.affiliate_url)

        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(1, len(root.findall('product')))
        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(
                self.course_run.key, self.seat_verified.type.slug))[0],
            self.seat_verified)

        # Add professional seat
        seat_professional = SeatFactory(course_run=self.course_run,
                                        type=SeatTypeFactory.professional())

        response = self.client.get(self.affiliate_url)
        root = ET.fromstring(response.content)
        self.assertEqual(2, len(root.findall('product')))

        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(
                self.course_run.key, self.seat_verified.type.slug))[0],
            self.seat_verified)
        self.assert_product_xml(
            root.findall('product/[pid="{}-{}"]'.format(
                self.course_run.key, seat_professional.type.slug))[0],
            seat_professional)

    @ddt.data(Seat.CREDIT, Seat.HONOR, Seat.AUDIT)
    def test_with_non_supported_seats(self, non_supporting_seat):
        """ Verify that endpoint returns no data for honor, credit and audit seats. """

        self.seat_verified.type = SeatType.objects.get_or_create(
            slug=non_supporting_seat)[0]
        self.seat_verified.save()

        response = self.client.get(self.affiliate_url)
        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(0, len(root.findall('product')))

    def test_with_closed_enrollment(self):
        """ Verify that endpoint returns no data if enrollment is close. """
        self.course_run.enrollment_end = datetime.datetime.now(
            pytz.UTC) - datetime.timedelta(days=100)
        self.course_run.end = datetime.datetime.now(
            pytz.UTC) - datetime.timedelta(days=100)
        self.course_run.save()

        # new course run with future end date and no enrollment_date.
        CourseRunFactory(end=self.course_end,
                         course=self.course,
                         enrollment_end=None)

        response = self.client.get(self.affiliate_url)

        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(0, len(root.findall('product')))

    def assert_product_xml(self, content, seat):
        """ Helper method to verify product data in xml format. """
        assert content.find('pid').text == '{}-{}'.format(
            self.course_run.key, seat.type.slug)
        assert content.find('name').text == self.course_run.title
        assert content.find('desc').text == self.course_run.full_description
        assert content.find('purl').text == self.course_run.marketing_url
        assert content.find('imgurl').text == self.course_run.image_url
        assert content.find('price/actualp').text == str(seat.price)
        assert content.find('currency').text == seat.currency.code
        assert content.find(
            'category').text == AffiliateWindowSerializer.CATEGORY

    def test_dtd_with_valid_data(self):
        """ Verify the XML data produced by the endpoint conforms to the DTD file. """
        response = self.client.get(self.affiliate_url)
        assert response.status_code == 200

        filename = abspath(
            join(dirname(dirname(__file__)),
                 'affiliate_window_product_feed.1.4.dtd'))
        dtd = etree.DTD(open(filename))
        root = etree.XML(response.content)
        assert dtd.validate(root)

    def test_permissions(self):
        """ Verify only users with the appropriate permissions can access the endpoint. """
        catalog = CatalogFactory()
        superuser = UserFactory(is_superuser=True)
        url = reverse('api:v1:partners:affiliate_window-detail',
                      kwargs={'pk': catalog.id})

        # Superusers can view all catalogs
        self.client.force_authenticate(superuser)

        with self.assertNumQueries(6, threshold=1):  # travis is often 7
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)

        # Regular users can only view catalogs belonging to them
        self.client.force_authenticate(self.user)
        response = self.client.get(url)
        self.assertEqual(response.status_code, 403)

        catalog.viewers = [self.user]
        with self.assertNumQueries(9, threshold=1):  # travis is often 10
            response = self.client.get(url)
            self.assertEqual(response.status_code, 200)

    def test_unpublished_status(self):
        """ Verify the endpoint does not return CourseRuns in a non-published state. """
        self.course_run.status = CourseRunStatus.Unpublished
        self.course_run.save()

        CourseRunFactory(course=self.course,
                         status=CourseRunStatus.Unpublished)

        response = self.client.get(self.affiliate_url)

        self.assertEqual(response.status_code, 200)
        root = ET.fromstring(response.content)
        self.assertEqual(0, len(root.findall('product')))
Ejemplo n.º 12
0
class TestLoadProgramFixture(TestCase):
    oauth_host = 'http://example.com'
    catalog_host = 'http://discovery-example.com'

    def setUp(self):
        super().setUp()
        self.pk_generator = itertools.count(1)

        stored_site, created = Site.objects.get_or_create(  # pylint: disable=unused-variable
            domain='example.com')
        self.default_partner = Partner.objects.create(site=stored_site,
                                                      name='edX',
                                                      short_code='edx')

        SeatType.objects.all().delete()
        ProgramType.objects.all().delete()

        self.partner = PartnerFactory(name='Test')
        self.organization = OrganizationFactory(partner=self.partner)
        self.seat_type_verified = SeatTypeFactory(name='Verified',
                                                  slug='verified')
        self.program_type_masters = ProgramTypeFactory(
            name='Masters',
            slug='masters',
            applicable_seat_types=[self.seat_type_verified])

        self.program_type_mm = ProgramTypeFactory(
            name='MicroMasters',
            slug='micromasters',
            applicable_seat_types=[self.seat_type_verified])

        self.course = CourseFactory(
            partner=self.partner, authoring_organizations=[self.organization])
        self.course_run = CourseRunFactory(course=self.course)
        self.program = ProgramFactory(
            type=self.program_type_masters,
            partner=self.partner,
            authoring_organizations=[self.organization])
        self.course_mm = CourseFactory(
            partner=self.partner, authoring_organizations=[self.organization])
        self.course_run_mm = CourseRunFactory(course=self.course)
        self.program_mm = ProgramFactory(
            type=self.program_type_mm,
            partner=self.partner,
            authoring_organizations=[self.organization],
            courses=[self.course_mm])
        self.curriculum = CurriculumFactory(program=self.program)
        self.curriculum_course_membership = CurriculumCourseMembershipFactory(
            course=self.course, curriculum=self.curriculum)
        self.curriculum_program_membership = CurriculumProgramMembershipFactory(
            program=self.program_mm, curriculum=self.curriculum)

        self.program_2 = ProgramFactory(
            type=self.program_type_masters,
            partner=self.partner,
            authoring_organizations=[self.organization])

        self._mock_oauth_request()

    def _mock_oauth_request(self):
        responses.add(
            responses.POST,
            f'{self.oauth_host}/oauth2/access_token',
            json={
                'access_token': 'abcd',
                'expires_in': 60
            },
            status=200,
        )

    def _mock_fixture_response(self, fixture):
        url = re.compile(
            '{catalog_host}/extensions/api/v1/program-fixture/'.format(
                catalog_host=self.catalog_host, ))
        responses.add(responses.GET, url, body=fixture, status=200)

    def _call_load_program_fixture(self, program_uuids):
        call_command(
            'load_program_fixture',
            ','.join(program_uuids),
            '--catalog-host',
            self.catalog_host,
            '--oauth-host',
            self.oauth_host,
            '--client-id',
            'foo',
            '--client-secret',
            'bar',
        )

    def _set_up_masters_program_type(self):
        """
        Set DB to have a conflicting program type on load.
        """
        seat_type = SeatTypeFactory(
            name='Something',
            slug='something',
        )
        existing_program_type = ProgramTypeFactory(
            name='Masters',
            name_t='Masters',
            slug='masters',
            applicable_seat_types=[seat_type])
        return existing_program_type

    def reset_db_state(self):
        Partner.objects.all().exclude(short_code='edx').delete()
        SeatType.objects.all().delete()
        Course.objects.all().delete()
        CourseRun.objects.all().delete()
        Curriculum.objects.all().delete()
        CurriculumCourseMembership.objects.all().delete()
        CurriculumProgramMembership.objects.all().delete()
        ProgramType.objects.all().delete()
        Organization.objects.all().delete()
        Program.objects.all().delete()

    @responses.activate
    def test_load_programs(self):

        fixture = json_serializer.Serializer().serialize([
            self.program_type_masters,
            self.program_type_mm,
            self.organization,
            self.seat_type_verified,
            self.program,
            self.program_2,
            self.program_mm,
            self.curriculum_program_membership,
            self.curriculum_course_membership,
            self.curriculum,
            self.course,
            self.course_mm,
            self.course_run,
            self.course_run_mm,
        ])
        self._mock_fixture_response(fixture)

        requested_programs = [
            str(self.program.uuid),
            str(self.program_2.uuid),
        ]
        self.reset_db_state()
        self._call_load_program_fixture(requested_programs)

        # walk through program structure to validate correct
        # objects have been created
        stored_program = Program.objects.get(uuid=self.program.uuid)
        stored_program_2 = Program.objects.get(uuid=self.program_2.uuid)
        self.assertEqual(stored_program.title, self.program.title)
        self.assertEqual(stored_program_2.title, self.program_2.title)

        stored_organization = stored_program.authoring_organizations.first()
        self.assertEqual(stored_organization.name, self.organization.name)

        # partner should use existing edx value
        self.assertEqual(stored_program.partner, self.default_partner)
        self.assertEqual(stored_organization.partner, self.default_partner)

        stored_program_type = stored_program.type
        self.assertEqual(stored_program_type.name_t,
                         self.program_type_masters.name)

        stored_seat_type = stored_program_type.applicable_seat_types.first()
        self.assertEqual(stored_seat_type.name, self.seat_type_verified.name)

        stored_curriculum = stored_program.curricula.first()
        self.assertEqual(stored_curriculum.uuid, self.curriculum.uuid)

        stored_course = stored_curriculum.course_curriculum.first()
        self.assertEqual(stored_course.key, self.course.key)

        stored_mm = stored_curriculum.program_curriculum.first()
        self.assertEqual(stored_mm.uuid, self.program_mm.uuid)

        stored_course_run = stored_course.course_runs.first()
        self.assertEqual(stored_course_run.key, self.course_run.key)

    @responses.activate
    def test_update_existing_program_type(self):

        fixture = json_serializer.Serializer().serialize([
            self.organization,
            self.seat_type_verified,
            self.program_type_masters,
            self.program,
        ])
        self._mock_fixture_response(fixture)
        self.reset_db_state()

        existing_program_type = self._set_up_masters_program_type()

        self._call_load_program_fixture([str(self.program.uuid)])

        stored_program = Program.objects.get(uuid=self.program.uuid)

        # assert existing DB value is used
        stored_program_type = stored_program.type
        self.assertEqual(stored_program_type, existing_program_type)

        # assert existing DB value is updated to match fixture
        stored_seat_types = list(
            stored_program_type.applicable_seat_types.all())
        self.assertEqual(len(stored_seat_types), 1)
        self.assertEqual(stored_seat_types[0].name,
                         self.seat_type_verified.name)

    @responses.activate
    def test_remapping_courserun_programtype(self):
        """
        Tests whether the remapping of program types works for the course run field that points to them
        """
        self.course_run.expected_program_type = self.program_type_masters
        self.course_run.save()
        fixture = json_serializer.Serializer().serialize([
            self.program_type_masters,
            self.program_type_mm,
            self.organization,
            self.seat_type_verified,
            self.program,
            self.program_mm,
            self.curriculum_program_membership,
            self.curriculum_course_membership,
            self.curriculum,
            self.course,
            self.course_mm,
            self.course_run,
        ])
        self._mock_fixture_response(fixture)
        self.reset_db_state()

        existing_program_type = self._set_up_masters_program_type()

        self._call_load_program_fixture([str(self.program.uuid)])

        stored_courserun = CourseRun.objects.get(key=self.course_run.key)
        stored_program_type = stored_courserun.expected_program_type

        self.assertEqual(existing_program_type, stored_program_type)

    @responses.activate
    def test_existing_seat_types(self):

        fixture = json_serializer.Serializer().serialize([
            self.organization,
            self.seat_type_verified,
            self.program_type_masters,
            self.program,
        ])
        self._mock_fixture_response(fixture)
        self.reset_db_state()

        # create existing verified seat with different pk than fixture and
        # a second seat type with the same pk but different values
        new_pk = self.seat_type_verified.id + 1
        SeatType.objects.create(id=new_pk, name='Verified', slug='verified')
        SeatType.objects.create(id=self.seat_type_verified.id,
                                name='Test',
                                slug='test')
        self._call_load_program_fixture([str(self.program.uuid)])

        stored_program = Program.objects.get(uuid=self.program.uuid)
        stored_seat_type = stored_program.type.applicable_seat_types.first()

        self.assertEqual(stored_seat_type.id, new_pk)
        self.assertEqual(stored_seat_type.name, self.seat_type_verified.name)

    @responses.activate
    def test_fail_on_save_error(self):

        fixture = json_serializer.Serializer().serialize([
            self.organization,
        ])

        #  Should not be able to save an organization without uuid
        fixture_json = json.loads(fixture)
        fixture_json[0]['fields']['uuid'] = None
        fixture = json.dumps(fixture_json)

        self._mock_fixture_response(fixture)
        self.reset_db_state()

        with pytest.raises(IntegrityError) as err:
            self._call_load_program_fixture([str(self.program.uuid)])
        expected_msg = fr'Failed to save course_metadata.Organization\(pk={self.organization.id}\):'
        assert re.match(expected_msg, str(err.value))

    @responses.activate
    def test_fail_on_constraint_error(self):

        # duplicate programs should successfully save but fail final constraint check
        fixture = json_serializer.Serializer().serialize([
            self.program,
            self.program,
            self.seat_type_verified,
            self.program_type_masters,
        ])
        self._mock_fixture_response(fixture)
        self.reset_db_state()

        with pytest.raises(IntegrityError) as err:
            self._call_load_program_fixture([str(self.program.uuid)])
        expected_msg = (
            r'Checking database constraints failed trying to load fixtures. Unable to save program\(s\):'
        ).format(pk=self.organization.id)
        assert re.match(expected_msg, str(err.value))

    @responses.activate
    def test_ignore_program_external_key(self):
        fixture = json_serializer.Serializer().serialize([
            self.organization,
            self.seat_type_verified,
            self.program_type_masters,
            self.program,
        ])
        self._mock_fixture_response(fixture)
        self.reset_db_state()

        self._call_load_program_fixture([
            '{uuid}:{external_key}'.format(uuid=str(self.program.uuid),
                                           external_key='CS-104-FALL-2019')
        ])

        Program.objects.get(uuid=self.program.uuid)

    @responses.activate
    def test_update_existing_data(self):

        fixture = json_serializer.Serializer().serialize([
            self.organization,
            self.seat_type_verified,
            self.program_type_masters,
            self.program,
            self.curriculum,
            self.course,
            self.course_run,
            self.curriculum_course_membership,
        ])
        self._mock_fixture_response(fixture)
        self._call_load_program_fixture([str(self.program.uuid)])

        self.program.title = 'program-title-modified'
        self.course.title = 'course-title-modified'
        new_course = CourseFactory(partner=self.partner,
                                   authoring_organizations=[self.organization])
        new_course_run = CourseRunFactory(course=new_course)
        new_course_membership = CurriculumCourseMembershipFactory(
            course=new_course, curriculum=self.curriculum)

        fixture = json_serializer.Serializer().serialize([
            self.organization,
            self.seat_type_verified,
            self.program_type_masters,
            self.program,
            self.curriculum,
            self.course,
            self.course_run,
            self.curriculum_course_membership,
            new_course_membership,
            new_course,
            new_course_run,
        ])
        responses.reset()
        self._mock_oauth_request()
        self._mock_fixture_response(fixture)
        self.reset_db_state()
        self._call_load_program_fixture([str(self.program.uuid)])

        stored_program = Program.objects.get(uuid=self.program.uuid)
        self.assertEqual(stored_program.title, 'program-title-modified')

        stored_program_courses = stored_program.curricula.first(
        ).course_curriculum.all()
        modified_existing_course = stored_program_courses.get(
            uuid=self.course.uuid)
        stored_new_course = stored_program_courses.get(uuid=new_course.uuid)

        self.assertEqual(len(stored_program_courses), 2)
        self.assertEqual(modified_existing_course.title,
                         'course-title-modified')
        self.assertEqual(stored_new_course.key, new_course.key)
Ejemplo n.º 13
0
class TestLoadDrupalData(TestCase):
    def setUp(self):
        super(TestLoadDrupalData, self).setUp()
        self.command_name = 'load_drupal_data'
        self.partner = PartnerFactory()
        self.course_run = CourseRunFactory(course__partner=self.partner)
        self.course_run.course.canonical_course_run = self.course_run
        self.course_run.course.save()

    def mock_access_token_api(self, requests_mock=None):
        body = {'access_token': ACCESS_TOKEN, 'expires_in': 30}
        requests_mock = requests_mock or responses

        url = self.partner.oidc_url_root.strip('/') + '/access_token'
        requests_mock.add_callback(responses.POST,
                                   url,
                                   callback=mock_api_callback(
                                       url, body, results_key=False),
                                   content_type='application/json')

        return body

    def test_load_drupal_data_with_partner(self):
        with responses.RequestsMock() as rsps:
            self.mock_access_token_api(rsps)

            with mock.patch(
                    'course_discovery.apps.publisher.management.commands.'
                    'load_drupal_data.execute_loader') as mock_executor:
                config = DrupalLoaderConfigFactory.create(
                    course_run_ids='course-v1:SC+BreadX+3T2015',
                    partner_code=self.partner.short_code,
                    load_unpublished_course_runs=False)
                call_command('load_drupal_data')

                expected_calls = [
                    mock.call(DrupalCourseMarketingSiteDataLoader,
                              self.partner,
                              self.partner.marketing_site_url_root,
                              ACCESS_TOKEN,
                              'JWT',
                              1,
                              False,
                              set(config.course_run_ids.split(',')),
                              config.load_unpublished_course_runs,
                              username=jwt.decode(
                                  ACCESS_TOKEN,
                                  verify=False)['preferred_username'])
                ]
                mock_executor.assert_has_calls(expected_calls)

    def test_process_node(self):
        # Set the end date in the future
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.max.strftime('%s')
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        config = DrupalLoaderConfigFactory.create(
            course_run_ids=data.get('field_course_id'),
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=False)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            config.load_unpublished_course_runs)

        # Need to mock this method so that the GET isn't sent out to the test data server
        with mock.patch(
                'course_discovery.apps.publisher.dataloader.create_courses.'
                'transfer_course_image'):
            data_loader.process_node(
                mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0])
            course_metadata_course_run = CourseMetadataCourseRun.objects.get(
                key=data.get('field_course_id'))
            self.assertIsNotNone(course_metadata_course_run)
            self.assertIsNotNone(course_metadata_course_run.course)
            publisher_course_run = PublisherCourseRun.objects.get(
                lms_course_id=course_metadata_course_run.key)
            self.assertIsNotNone(publisher_course_run)
            self.assertIsNotNone(publisher_course_run.course)

    def test_process_node_archived(self):
        # Set the end date in the past
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.min.strftime('%s')
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        config = DrupalLoaderConfigFactory.create(
            course_run_ids=data.get('field_course_id'),
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=False)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            config.load_unpublished_course_runs)

        # Need to mock this method so that the GET isn't sent out to the test data server
        with mock.patch(
                'course_discovery.apps.publisher.dataloader.create_courses.'
                'transfer_course_image'):
            data_loader.process_node(
                mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0])
            course_metadata_course_run = CourseMetadataCourseRun.objects.filter(
                key=data.get('field_course_id'))
            self.assertEqual(course_metadata_course_run.count(), 0)

    def test_process_node_not_whitelisted(self):
        # Set the end date in the future
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.max.strftime('%s')
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        config = DrupalLoaderConfigFactory.create(
            course_run_ids='SomeFakeCourseRunId',
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=False)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            config.load_unpublished_course_runs)

        # Need to mock this method so that the GET isn't sent out to the test data server
        with mock.patch(
                'course_discovery.apps.publisher.dataloader.create_courses.'
                'transfer_course_image'):
            for body in mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES:
                data_loader.process_node(body)
            # Even after looping through all course bodies no new rows should be created
            course_metadata_course_run = CourseMetadataCourseRun.objects.filter(
                key=data.get('field_course_id'))
            self.assertEqual(course_metadata_course_run.count(), 0)

    def test_process_node_run_created(self):
        # Set the end date in the future
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.max.strftime('%s')
        data['status'] = '1'
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        self.course_run.key = data.get('field_course_id')
        self.course_run.save()

        config = DrupalLoaderConfigFactory.create(
            course_run_ids=data.get('field_course_id'),
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=False)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            config.load_unpublished_course_runs)

        with mock.patch(
                'course_discovery.apps.publisher.signals.create_course_run_in_studio_receiver'
        ) as mock_signal:
            # Need to mock this method so that the GET isn't sent out to the test data server
            with mock.patch(
                    'course_discovery.apps.publisher.dataloader.create_courses.'
                    'transfer_course_image'):
                data_loader.process_node(
                    mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0])
                mock_signal.assert_not_called()

        course_metadata_course_run = CourseMetadataCourseRun.objects.get(
            key=data.get('field_course_id'))
        self.assertIsNotNone(course_metadata_course_run)
        self.assertIsNotNone(course_metadata_course_run.course)
        publisher_course_run = PublisherCourseRun.objects.get(
            lms_course_id=course_metadata_course_run.key)
        self.assertIsNotNone(publisher_course_run)
        self.assertIsNotNone(publisher_course_run.course)

    def test_load_unpublished_course_runs_with_flag_enabled(self):
        # Set the end date in the future
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.max.strftime('%s')
        # Set the status to unpublished
        data['status'] = '0'
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        self.course_run.key = data.get('field_course_id')
        self.course_run.save()

        PublisherCourseFactory.create(
            course_metadata_pk=self.course_run.course.id)

        load_unpublished_course_runs = True

        config = DrupalLoaderConfigFactory.create(
            course_run_ids=data.get('field_course_id'),
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=load_unpublished_course_runs)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            load_unpublished_course_runs)

        # Need to mock this method so that the GET isn't sent out to the test data server
        with mock.patch(
                'course_discovery.apps.publisher.dataloader.create_courses.'
                'transfer_course_image'):
            data_loader.process_node(
                mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0])

        course_metadata_course_run = CourseMetadataCourseRun.objects.get(
            key=data.get('field_course_id'))
        self.assertIsNotNone(course_metadata_course_run)
        self.assertIsNotNone(course_metadata_course_run.course)
        publisher_course_run = PublisherCourseRun.objects.get(
            lms_course_id=course_metadata_course_run.key)
        self.assertIsNotNone(publisher_course_run)
        self.assertIsNotNone(publisher_course_run.course)

    def test_load_unpublished_course_runs_with_flag_enabled_no_course_found(
            self):
        # Set the end date in the future
        data = mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0]
        data['field_course_end_date'] = datetime.datetime.max.strftime('%s')
        # Set the status to unpublished
        data['status'] = '0'
        OrganizationFactory.create(
            uuid=data.get('field_course_school_node', {})[0].get('uuid'))

        self.course_run.key = data.get('field_course_id')
        self.course_run.save()

        load_unpublished_course_runs = True

        config = DrupalLoaderConfigFactory.create(
            course_run_ids=data.get('field_course_id'),
            partner_code=self.partner.short_code,
            load_unpublished_course_runs=load_unpublished_course_runs)
        data_loader = DrupalCourseMarketingSiteDataLoader(
            self.partner,
            self.partner.marketing_site_url_root,
            ACCESS_TOKEN,
            'JWT',
            1,  # Make this a constant of 1 for no concurrency
            False,
            set(config.course_run_ids.split(',')),
            load_unpublished_course_runs)

        logger_target = 'course_discovery.apps.publisher.management.commands.load_drupal_data.logger'
        with mock.patch(logger_target) as mock_logger:
            # Need to mock this method so that the GET isn't sent out to the test data server
            with mock.patch(
                    'course_discovery.apps.publisher.dataloader.create_courses.'
                    'transfer_course_image'):
                data_loader.process_node(
                    mock_data.UNIQUE_MARKETING_SITE_API_COURSE_BODIES[0])
            expected_calls = [
                mock.call('No Publisher Course found for Course Run [%s]',
                          self.course_run.key)
            ]
            mock_logger.info.assert_has_calls(expected_calls)
Ejemplo n.º 14
0
class CourseRunSerializerTests(TestCase):
    serializer_class = CourseRunSerializer

    def setUp(self):
        super(CourseRunSerializerTests, self).setUp()
        self.course_run = CourseRunFactory()
        self.course_run.lms_course_id = 'course-v1:edX+DemoX+Demo_Course'
        self.person = PersonFactory()
        self.discovery_course_run = DiscoveryCourseRunFactory(
            key=self.course_run.lms_course_id, staff=[self.person])
        self.request = RequestFactory()
        self.user = UserFactory()
        self.request.user = self.user
        self.course_state = CourseRunStateFactory(
            course_run=self.course_run, owner_role=PublisherUserRole.Publisher)

    def get_expected_data(self):
        """ Helper method which will return expected serialize data. """
        return {
            'lms_course_id': self.course_run.lms_course_id,
            'changed_by': self.user,
            'preview_url': self.course_run.preview_url
        }

    def test_validate_lms_course_id(self):
        """ Verify that serializer raises error if 'lms_course_id' has invalid format. """
        self.course_run.lms_course_id = 'invalid-course-id'
        self.course_run.save()
        serializer = self.serializer_class(self.course_run)
        with self.assertRaises(ValidationError):
            serializer.validate_lms_course_id(self.course_run.lms_course_id)

    def test_validate_preview_url(self):
        """ Verify that serializer raises error if 'preview_url' has invalid format. """
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})
        with self.assertRaises(ValidationError):
            serializer.validate({'preview_url': 'invalid-url'})

    def test_serializer_with_valid_data(self):
        """ Verify that serializer validate course_run object. """
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})
        self.assertEqual(self.get_expected_data(),
                         serializer.validate(serializer.data))

    def test_update_preview_url(self):
        """ Verify that course 'owner_role' will be changed to course_team after updating
        course run with preview url.
        """
        self.discovery_course_run.slug = ''
        self.discovery_course_run.save(suppress_publication=True)
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})
        serializer.update(self.course_run,
                          {'preview_url': 'https://example.com/abc/course'})

        self.assertEqual(self.course_state.owner_role,
                         PublisherUserRole.CourseTeam)
        self.assertEqual(
            self.course_run.preview_url.rsplit('/', 1)[-1], 'course')

    def test_update_preview_url_no_op(self):
        """ Verify we don't push to marketing if no change required """
        self.discovery_course_run.slug = ''
        self.discovery_course_run.save(suppress_publication=True)

        toggle_switch('publish_course_runs_to_marketing_site')
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})

        mock_path = 'course_discovery.apps.course_metadata.publishers.CourseRunMarketingSitePublisher.publish_obj'
        with mock.patch(mock_path) as mock_save:
            serializer.update(
                self.course_run,
                {'preview_url': 'https://example.com/abc/course'})
            self.assertEqual(mock_save.call_count, 1)

            # Now when we update a second time, there should be nothing to do, call count should remain at 1
            serializer.update(
                self.course_run,
                {'preview_url': 'https://example.com/abc/course'})
            self.assertEqual(mock_save.call_count, 1)

    def test_update_preview_url_slug_exists(self):
        """ Verify we don't push to marketing if no change required """
        DiscoveryCourseRunFactory(
            title='course')  # will create the slug 'course'
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})

        with self.assertRaises(Exception) as cm:
            serializer.update(
                self.course_run,
                {'preview_url': 'https://example.com/abc/course'})
        self.assertEqual(cm.exception.args[0],
                         'Preview URL already in use for another course')

    def test_update_lms_course_id(self):
        """ Verify that 'changed_by' also updated after updating course_run's lms_course_id."""
        serializer = self.serializer_class(self.course_run,
                                           context={'request': self.request})
        serializer.update(self.course_run,
                          serializer.validate(serializer.data))

        self.assertEqual(self.course_run.lms_course_id,
                         serializer.data['lms_course_id'])
        self.assertEqual(self.course_run.changed_by, self.user)

    def test_update_with_transaction_rollback(self):
        """
        Verify that transaction roll backed if an error occurred.
        """
        serializer = self.serializer_class(self.course_run)

        with self.assertRaises(Exception):
            serializer.update(self.course_run, {'preview_url': 'invalid_url'})
            self.assertFalse(self.course_run.preview_url)

    def test_transaction_roll_back_with_error_on_email(self):
        """
        Verify that transaction is roll backed if error occurred during email sending.
        """
        toggle_switch('enable_publisher_email_notifications', True)
        serializer = self.serializer_class(self.course_run)
        self.assertEqual(self.course_run.course_run_state.owner_role,
                         PublisherUserRole.Publisher)

        with self.assertRaises(Exception):
            serializer.update(
                self.course_run,
                {'preview_url': 'https://example.com/abc/course'})

        self.course_run = CourseRun.objects.get(id=self.course_run.id)
        self.assertFalse(self.course_run.preview_url)
        # Verify that owner role not changed.
        self.assertEqual(self.course_run.course_run_state.owner_role,
                         PublisherUserRole.Publisher)