Exemplo n.º 1
0
    def test_unfulfilled_entitlement(self, mock_course_overview,
                                     mock_pseudo_session, mock_course_runs,
                                     mock_get_programs):
        """
        When a learner has an unfulfilled entitlement, their course dashboard should have:
            - a hidden 'View Course' button
            - the text 'In order to view the course you must select a session:'
            - an unhidden course-entitlement-selection-container
            - a related programs message
        """
        program = ProgramFactory()
        CourseEntitlementFactory.create(
            user=self.user, course_uuid=program['courses'][0]['uuid'])
        mock_get_programs.return_value = [program]
        mock_course_overview.return_value = CourseOverviewFactory.create(
            start=self.TOMORROW)
        mock_course_runs.return_value = [{
            'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
            'enrollment_end': str(self.TOMORROW),
            'pacing_type': 'instructor_paced',
            'type': 'verified',
            'status': 'published'
        }]
        mock_pseudo_session.return_value = {
            'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
            'type': 'verified'
        }
        response = self.client.get(self.path)
        self.assertIn('class="enter-course hidden"', response.content)
        self.assertIn('You must select a session to access the course.',
                      response.content)
        self.assertIn('<div class="course-entitlement-selection-container ">',
                      response.content)
        self.assertIn('Related Programs:', response.content)

        # If an entitlement has already been redeemed by the user for a course run, do not let the run be selectable
        enrollment = CourseEnrollmentFactory(
            user=self.user,
            course_id=unicode(mock_course_overview.return_value.id),
            mode=CourseMode.VERIFIED)
        CourseEntitlementFactory.create(
            user=self.user,
            course_uuid=program['courses'][0]['uuid'],
            enrollment_course_run=enrollment)

        mock_course_runs.return_value = [{
            'key': 'course-v1:edX+toy+2012_Fall',
            'enrollment_end': str(self.TOMORROW),
            'pacing_type': 'instructor_paced',
            'type': 'verified',
            'status': 'published'
        }]
        response = self.client.get(self.path)
        # There should be two entitlements on the course page, one prompting for a mandatory session, but no
        # select option for the courses as there is only the single course run which has already been redeemed
        self.assertEqual(response.content.count('<li class="course-item">'), 2)
        self.assertIn('You must select a session to access the course.',
                      response.content)
        self.assertNotIn('To access the course, select a session.',
                         response.content)
    def _create_cached_program(self):
        """ helper method to create a cached program """
        program = ProgramFactory.create()

        for course_key in self.course_keys:
            program['courses'].append(CourseFactory(id=course_key))

        program['type'] = 'MicroBachelors'
        program['type_attrs']['coaching_supported'] = True

        for course in program['courses']:
            cache.set(
                CATALOG_COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_uuid=course['uuid']),
                [program['uuid']],
                None
            )

            course_run = course['course_runs'][0]['key']
            cache.set(
                COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_run_id=course_run),
                [program['uuid']],
                None
            )
        cache.set(
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']),
            program,
            None
        )

        return program
Exemplo n.º 3
0
 def test_fulfilled_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
     """
     When a learner has a fulfilled entitlement, their course dashboard should have:
         - exactly one course item, meaning it:
             - has an entitlement card
             - does NOT have a course card referencing the selected session
         - an unhidden Change or Leave Session button
         - a related programs message
     """
     mocked_course_overview = CourseOverviewFactory(
         start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
     )
     mock_course_overview.return_value = mocked_course_overview
     mock_course_key.return_value = mocked_course_overview.id
     course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id))
     mock_course_runs.return_value = [
         {
             'key': str(mocked_course_overview.id),
             'enrollment_end': str(mocked_course_overview.enrollment_end),
             'pacing_type': 'self_paced',
             'type': 'verified',
             'status': 'published'
         }
     ]
     entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
     program = ProgramFactory()
     program['courses'][0]['course_runs'] = [{'key': unicode(mocked_course_overview.id)}]
     program['courses'][0]['uuid'] = entitlement.course_uuid
     mock_get_programs.return_value = [program]
     response = self.client.get(self.path)
     self.assertEqual(response.content.count('<li class="course-item">'), 1)
     self.assertIn('<button class="change-session btn-link "', response.content)
     self.assertIn('Related Programs:', response.content)
Exemplo n.º 4
0
 def test_fulfilled_expired_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs, mock_get_programs):
     """
     When a learner has a fulfilled entitlement that is expired, their course dashboard should have:
         - exactly one course item, meaning it:
             - has an entitlement card
         - Message that the learner can no longer change sessions
         - a related programs message
     """
     mocked_course_overview = CourseOverviewFactory(
         start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
     )
     mock_course_overview.return_value = mocked_course_overview
     mock_course_key.return_value = mocked_course_overview.id
     course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id), created=self.THREE_YEARS_AGO)
     mock_course_runs.return_value = [
         {
             'key': str(mocked_course_overview.id),
             'enrollment_end': str(mocked_course_overview.enrollment_end),
             'pacing_type': 'self_paced',
             'type': 'verified',
             'status': 'published'
         }
     ]
     entitlement = CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment, created=self.THREE_YEARS_AGO)
     program = ProgramFactory()
     program['courses'][0]['course_runs'] = [{'key': unicode(mocked_course_overview.id)}]
     program['courses'][0]['uuid'] = entitlement.course_uuid
     mock_get_programs.return_value = [program]
     response = self.client.get(self.path)
     self.assertEqual(response.content.count('<li class="course-item">'), 1)
     self.assertIn('You can no longer change sessions.', response.content)
     self.assertIn('Related Programs:', response.content)
    def test_handle_no_course_overview(self, mock_task, mock_get_programs):
        """
        Verify that the task is not enqueued for a user whose only certificate
        is for a course with no CourseOverview.
        """
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=self.course_run_key),
                    CourseRunFactory(
                        key=self.course_run_key_no_course_overview),
                ]),
            ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )
        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_run_key_no_course_overview,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
        mock_task.assert_not_called(self.bob.username)
    def test_handle(self, commit, mock_task, mock_get_programs):
        """
        Verify that relevant tasks are only enqueued when the commit option is passed.
        """
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=self.course_run_key),
                ]),
            ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=commit)

        if commit:
            mock_task.assert_called_once_with(self.alice.username)
        else:
            mock_task.assert_not_called()
    def test_handle_username_dedup(self, mock_task, mock_get_programs):
        """
        Verify that only one task is enqueued for a user with multiple eligible
        course run certificates.
        """
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=self.course_run_key),
                    CourseRunFactory(key=self.alternate_course_run_key),
                ]),
            ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.alternate_course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
    def test_handle_mode_slugs(self, mock_task, mock_get_programs):
        """
        Verify that course run types are taken into account when identifying
        qualifying course run certificates.
        """
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=self.course_run_key, type='honor'),
                ]),
            ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_run_key,
            mode=MODES.honor,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_run_key,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
Exemplo n.º 9
0
    def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info):
        programs = ProgramFactory.create_batch(3)

        all_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs
        }

        partial_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2]
        }

        def fake_get_many(keys):
            if len(keys) == 1:
                return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]}
            else:
                return partial_programs

        mock_cache.get.return_value = [program['uuid'] for program in programs]
        mock_cache.get_many.side_effect = fake_get_many

        with with_site_configuration_context(domain=self.site.name, configuration={'COURSE_CATALOG_API_URL': 'foo'}):
            actual_programs = get_programs(site=self.site)

        # All 3 cached programs should be returned. An info message should be
        # logged about the one that was initially missing, but the code should
        # be able to stitch together all the details.
            assert {program['uuid'] for program in actual_programs} ==\
                   {program['uuid'] for program in all_programs.values()}
            assert not mock_warning.called
            mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.')

            for program in actual_programs:
                key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
                assert program == all_programs[key]
    def test_handle_professional(self, mock_task, mock_get_programs):
        """ Verify the task can handle both professional and no-id-professional modes. """
        mock_get_programs.return_value = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=self.course_run_key,
                                     type='professional'),
                ]),
            ]),
        ]

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_run_key,
            mode=CourseMode.PROFESSIONAL,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_run_key,
            mode=CourseMode.NO_ID_PROFESSIONAL_MODE,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        # The task should be called for both users since professional and no-id-professional are equivalent.
        mock_task.assert_has_calls(
            [mock.call(self.alice.username),
             mock.call(self.bob.username)],
            any_order=True)
Exemplo n.º 11
0
    def create_program(self):
        """DRY helper for creating test program data."""
        course_run = CourseRunFactory(key=self.course_id)
        course = CourseFactory(course_runs=[course_run])

        program_type = ProgramTypeFactory()
        return ProgramFactory(courses=[course], type=program_type['name'])
Exemplo n.º 12
0
    def setUp(self):
        super(ProgramPageBase, self).setUp()

        self.set_programs_api_configuration(is_enabled=True)

        self.programs = ProgramFactory.create_batch(3)
        self.username = None
Exemplo n.º 13
0
    def test_completed_programs_no_id_professional(self,
                                                   mock_completed_course_runs,
                                                   mock_get_programs):
        """ Verify the method treats no-id-professional enrollments as professional enrollments. """
        course_runs = CourseRunFactory.create_batch(2,
                                                    type='no-id-professional')
        program = ProgramFactory(
            courses=[CourseFactory(course_runs=course_runs)])
        mock_get_programs.return_value = [program]

        # Verify that no programs are complete.
        meter = ProgramProgressMeter(self.user)
        self.assertEqual(meter.completed_programs, [])

        # Complete all programs.
        for course_run in course_runs:
            CourseEnrollmentFactory(user=self.user,
                                    course_id=course_run['key'],
                                    mode='no-id-professional')

        mock_completed_course_runs.return_value = [{
            'course_run_id':
            course_run['key'],
            'type':
            MODES.professional
        } for course_run in course_runs]

        # Verify that all programs are complete.
        meter = ProgramProgressMeter(self.user)
        self.assertEqual(meter.completed_programs, [program['uuid']])
Exemplo n.º 14
0
    def test_multiple_published_course_runs(self):
        """
        Learner should not be eligible for one click purchase if:
            - program has a course with more than one published course run
        """
        course_run_1 = CourseRunFactory(key=str(ModuleStoreCourseFactory().id),
                                        status='published')
        course_run_2 = CourseRunFactory(key=str(ModuleStoreCourseFactory().id),
                                        status='published')
        course = CourseFactory(course_runs=[course_run_1, course_run_2])
        program = ProgramFactory(
            courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=str(ModuleStoreCourseFactory().id),
                                     status='published')
                ]), course,
                CourseFactory(course_runs=[
                    CourseRunFactory(key=str(ModuleStoreCourseFactory().id),
                                     status='published')
                ])
            ],
            is_program_eligible_for_one_click_purchase=True)
        data = ProgramMarketingDataExtender(program, self.user).extend()

        self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])

        course_run_2['status'] = 'unpublished'
        data = ProgramMarketingDataExtender(program, self.user).extend()

        self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
Exemplo n.º 15
0
    def test_course_progress(self, mock_get_programs):
        """
        Verify that the progress meter can represent progress in terms of
        serialized courses.
        """
        course_run_key = generate_course_run_key()
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=course_run_key),
                ]),
            ])
        ]
        mock_get_programs.return_value = data

        self._create_enrollments(course_run_key)

        meter = ProgramProgressMeter(self.site, self.user)

        program = data[0]
        expected = [
            ProgressFactory(uuid=program['uuid'],
                            completed=[],
                            in_progress=[program['courses'][0]],
                            not_started=[])
        ]

        self.assertEqual(meter.progress(count_only=False), expected)
    def setUp(self):
        super(TestCachePrograms, self).setUp()

        httpretty.httpretty.reset()

        self.catalog_integration = self.create_catalog_integration()
        self.site_domain = 'testsite.com'
        self.set_up_site(
            self.site_domain,
            {
                'COURSE_CATALOG_API_URL': self.catalog_integration.get_internal_api_url().rstrip('/')
            }
        )

        self.list_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/programs/'
        self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/'
        self.pathway_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/pathways/'

        self.programs = ProgramFactory.create_batch(3)
        self.pathways = PathwayFactory.create_batch(3)

        for pathway in self.pathways:
            self.programs += pathway['programs']

        self.uuids = [program['uuid'] for program in self.programs]

        # add some of the previously created programs to some pathways
        self.pathways[0]['programs'].extend([self.programs[0], self.programs[1]])
        self.pathways[1]['programs'].append(self.programs[0])
Exemplo n.º 17
0
    def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info):
        programs = ProgramFactory.create_batch(3)

        all_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs
        }

        partial_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2]
        }

        def fake_get_many(keys):
            if len(keys) == 1:
                return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]}
            else:
                return partial_programs

        mock_cache.get.return_value = [program['uuid'] for program in programs]
        mock_cache.get_many.side_effect = fake_get_many

        actual_programs = get_programs(self.site)

        # All 3 cached programs should be returned. An info message should be
        # logged about the one that was initially missing, but the code should
        # be able to stitch together all the details.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in all_programs.values())
        )
        self.assertFalse(mock_warning.called)
        mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.')

        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, all_programs[key])
Exemplo n.º 18
0
 def _create_catalog_program(self, catalog_org):
     """ helper method to create a cached catalog program """
     program = ProgramFactory.create(
         authoring_organizations=[catalog_org]
     )
     cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']), program, None)
     return program
Exemplo n.º 19
0
    def test_completed_programs(self, mock_completed_course_runs, mock_get_programs):
        """Verify that completed programs are correctly identified."""
        data = ProgramFactory.create_batch(3)
        mock_get_programs.return_value = data

        program_uuids = []
        course_run_keys = []
        for program in data:
            program_uuids.append(program['uuid'])

            for course in program['courses']:
                for course_run in course['course_runs']:
                    course_run_keys.append(course_run['key'])

        # Verify that no programs are complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, [])

        # Complete all programs.
        self._create_enrollments(*course_run_keys)
        mock_completed_course_runs.return_value = [
            {'course_run_id': course_run_key, 'type': MODES.verified}
            for course_run_key in course_run_keys
        ]

        # Verify that all programs are complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, program_uuids)
Exemplo n.º 20
0
    def setUp(self):
        super(ProgramPageBase, self).setUp()

        self.set_programs_api_configuration(is_enabled=True)

        self.programs = ProgramFactory.create_batch(3)
        self.username = None
Exemplo n.º 21
0
 def test_unfulfilled_entitlement(self, mock_course_overview,
                                  mock_pseudo_session, mock_course_runs,
                                  mock_get_programs):
     """
     When a learner has an unfulfilled entitlement, their course dashboard should have:
         - a hidden 'View Course' button
         - the text 'In order to view the course you must select a session:'
         - an unhidden course-entitlement-selection-container
         - a related programs message
     """
     program = ProgramFactory()
     CourseEntitlementFactory(user=self.user,
                              course_uuid=program['courses'][0]['uuid'])
     mock_get_programs.return_value = [program]
     mock_course_overview.return_value = CourseOverviewFactory(
         start=self.TOMORROW)
     mock_course_runs.return_value = [{
         'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
         'enrollment_end': str(self.TOMORROW),
         'pacing_type': 'instructor_paced',
         'type': 'verified'
     }]
     mock_pseudo_session.return_value = {
         'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
         'type': 'verified'
     }
     response = self.client.get(self.path)
     self.assertIn('class="enter-course hidden"', response.content)
     self.assertIn('You must select a session to access the course.',
                   response.content)
     self.assertIn('<div class="course-entitlement-selection-container ">',
                   response.content)
     self.assertIn('Related Programs:', response.content)
Exemplo n.º 22
0
    def setUpClass(cls):
        super().setUpClass()

        modulestore_course = ModuleStoreCourseFactory()
        course_run = CourseRunFactory(key=str(modulestore_course.id))
        course = CourseFactory(course_runs=[course_run])
        cls.program = ProgramFactory(uuid=cls.program_uuid, courses=[course])
Exemplo n.º 23
0
    def setUp(self):
        super(TestGetCertificates, self).setUp()

        self.user = UserFactory()
        self.program = ProgramFactory()
        self.course_certificate_url = 'fake-course-certificate-url'
        self.program_certificate_url = 'fake-program-certificate-url'
Exemplo n.º 24
0
    def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info):
        programs = ProgramFactory.create_batch(3)

        all_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs
        }

        partial_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2]
        }

        def fake_get_many(keys):
            if len(keys) == 1:
                return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]}
            else:
                return partial_programs

        mock_cache.get.return_value = [program['uuid'] for program in programs]
        mock_cache.get_many.side_effect = fake_get_many

        actual_programs = get_programs(site=self.site)

        # All 3 cached programs should be returned. An info message should be
        # logged about the one that was initially missing, but the code should
        # be able to stitch together all the details.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in all_programs.values())
        )
        self.assertFalse(mock_warning.called)
        mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.')

        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, all_programs[key])
Exemplo n.º 25
0
    def test_program_completion_with_no_id_professional(
            self, mock_get_certificates_for_user, mock_get_programs):
        """
        Verify that 'no-id-professional' certificates are treated as if they were
        'professional' certificates when determining program completion.
        """
        # Create serialized course runs like the ones we expect to receive from
        # the discovery service's API. These runs are of type 'professional'.
        course_runs = CourseRunFactory.create_batch(2, type='professional')
        program = ProgramFactory(
            courses=[CourseFactory(course_runs=course_runs)])
        mock_get_programs.return_value = [program]

        # Verify that the test program is not complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, [])

        # Grant a 'no-id-professional' certificate for one of the course runs,
        # thereby completing the program.
        mock_get_certificates_for_user.return_value = [
            self._make_certificate_result(status='downloadable',
                                          type='no-id-professional',
                                          course_key=course_runs[0]['key'])
        ]

        # Verify that the program is complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, [program['uuid']])
Exemplo n.º 26
0
    def test_completed_programs(self, mock_completed_course_runs,
                                mock_get_programs):
        """Verify that completed programs are correctly identified."""
        data = ProgramFactory.create_batch(3)
        mock_get_programs.return_value = data

        program_uuids = []
        course_run_keys = []
        for program in data:
            program_uuids.append(program['uuid'])

            for course in program['courses']:
                for course_run in course['course_runs']:
                    course_run_keys.append(course_run['key'])

        # Verify that no programs are complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, [])

        # Complete all programs.
        self._create_enrollments(*course_run_keys)
        mock_completed_course_runs.return_value = [{
            'course_run_id': course_run_key,
            'type': MODES.verified
        } for course_run_key in course_run_keys]

        # Verify that all programs are complete.
        meter = ProgramProgressMeter(self.site, self.user)
        self.assertEqual(meter.completed_programs, program_uuids)
Exemplo n.º 27
0
 def test_empty_programs(self, mock_get_programs):
     """Verify that programs with no courses do not count as completed."""
     program = ProgramFactory()
     program['courses'] = []
     meter = ProgramProgressMeter(self.site, self.user)
     program_complete = meter._is_program_complete(program)
     self.assertFalse(program_complete)
Exemplo n.º 28
0
    def test_no_id_professional_in_progress(self, mock_get_programs):
        """
        Verify that the progress meter treats no-id-professional enrollments
        as professional.
        """
        course_run_key = generate_course_run_key()
        data = [
            ProgramFactory(courses=[
                CourseFactory(course_runs=[
                    CourseRunFactory(key=course_run_key,
                                     type=CourseMode.PROFESSIONAL),
                ]),
            ])
        ]
        mock_get_programs.return_value = data

        CourseEnrollmentFactory(user=self.user,
                                course_id=course_run_key,
                                mode=CourseMode.NO_ID_PROFESSIONAL_MODE)

        meter = ProgramProgressMeter(self.site, self.user)

        program = data[0]
        expected = [
            ProgressFactory(uuid=program['uuid'],
                            completed=[],
                            in_progress=[program['courses'][0]],
                            not_started=[])
        ]

        self.assertEqual(meter.progress(count_only=False), expected)
 def _get_programs_data(self, hierarchy_type):
     """
     Generate a mock response for get_programs() with the given type of
     course hierarchy.  Dramatically simplifies (and makes consistent
     between test runs) the ddt-generated test_flatten methods.
     """
     if hierarchy_type == self.SEPARATE_PROGRAMS:
         return [
             ProgramFactory(
                 courses=[
                     CourseFactory(course_runs=[
                         CourseRunFactory(key=self.course_run_key),
                     ]),
                 ]
             ),
             ProgramFactory(
                 courses=[
                     CourseFactory(course_runs=[
                         CourseRunFactory(key=self.alternate_course_run_key),
                     ]),
                 ]
             ),
         ]
     elif hierarchy_type == self.SEPARATE_COURSES:
         return [
             ProgramFactory(
                 courses=[
                     CourseFactory(course_runs=[
                         CourseRunFactory(key=self.course_run_key),
                     ]),
                     CourseFactory(course_runs=[
                         CourseRunFactory(key=self.alternate_course_run_key),
                     ]),
                 ]
             ),
         ]
     else:  # SAME_COURSE
         return [
             ProgramFactory(
                 courses=[
                     CourseFactory(course_runs=[
                         CourseRunFactory(key=self.course_run_key),
                         CourseRunFactory(key=self.alternate_course_run_key),
                     ]),
                 ]
             ),
         ]
Exemplo n.º 30
0
    def test_get_one_program(self, mock_get_edx_api_data):
        program = ProgramFactory()
        mock_get_edx_api_data.return_value = program

        data = get_programs(uuid=self.uuid)

        self.assert_contract(mock_get_edx_api_data.call_args, program_uuid=self.uuid)
        self.assertEqual(data, program)
Exemplo n.º 31
0
    def setUpClass(cls):
        super(TestProgramDetails, cls).setUpClass()

        modulestore_course = ModuleStoreCourseFactory()
        course_run = CourseRunFactory(key=unicode(modulestore_course.id))  # pylint: disable=no-member
        course = CourseFactory(course_runs=[course_run])

        cls.data = ProgramFactory(uuid=cls.program_uuid, courses=[course])
Exemplo n.º 32
0
    def test_get_programs(self, mock_get_edx_api_data):
        programs = [ProgramFactory() for __ in range(3)]
        mock_get_edx_api_data.return_value = programs

        data = get_programs()

        self.assert_contract(mock_get_edx_api_data.call_args)
        self.assertEqual(data, programs)
Exemplo n.º 33
0
    def test_get_programs_by_types(self, mock_get_edx_api_data):
        programs = ProgramFactory.create_batch(2)
        mock_get_edx_api_data.return_value = programs

        data = get_programs(types=self.types)

        self.assert_contract(mock_get_edx_api_data.call_args, types=self.types)
        self.assertEqual(data, programs)
Exemplo n.º 34
0
 def setUp(self):
     super(GetUsersByExternalKeysTests, self).setUp()
     catalog_org = CatalogOrganizationFactory.create(key=self.organization_key)
     program = ProgramFactory.create(
         uuid=self.program_uuid,
         authoring_organizations=[catalog_org]
     )
     cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None)
Exemplo n.º 35
0
    def test_get_programs_by_types(self, mock_get_edx_api_data):
        programs = ProgramFactory.create_batch(2)
        mock_get_edx_api_data.return_value = programs

        data = get_programs(types=self.types)

        self.assert_contract(mock_get_edx_api_data.call_args, types=self.types)
        self.assertEqual(data, programs)
Exemplo n.º 36
0
    def setUp(self):
        super(TestProgramMarketingDataExtender, self).setUp()

        self.course_price = 100
        self.number_of_courses = 2
        self.program = ProgramFactory(
            courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)]
        )
Exemplo n.º 37
0
    def test_get_many(self, mock_warning):
        programs = ProgramFactory.create_batch(3)

        # Cache details for 2 of 3 programs.
        partial_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program
            for program in programs[:2]
        }
        cache.set_many(partial_programs, None)

        # When called before UUIDs are cached, the function should return an empty
        # list and log a warning.
        self.assertEqual(get_programs(), [])
        mock_warning.assert_called_once_with('Program UUIDs are not cached.')
        mock_warning.reset_mock()

        # Cache UUIDs for all 3 programs.
        cache.set(PROGRAM_UUIDS_CACHE_KEY,
                  [program['uuid'] for program in programs], None)

        actual_programs = get_programs()

        # The 2 cached programs should be returned while a warning should be logged
        # for the missing one.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in partial_programs.values()))
        mock_warning.assert_called_with(
            'Details for program {uuid} are not cached.'.format(
                uuid=programs[2]['uuid']))
        mock_warning.reset_mock()

        # We can't use a set comparison here because these values are dictionaries
        # and aren't hashable. We've already verified that all programs came out
        # of the cache above, so all we need to do here is verify the accuracy of
        # the data itself.
        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, partial_programs[key])

        # Cache details for all 3 programs.
        all_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program
            for program in programs
        }
        cache.set_many(all_programs, None)

        actual_programs = get_programs()

        # All 3 programs should be returned.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in all_programs.values()))
        self.assertFalse(mock_warning.called)

        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, all_programs[key])
Exemplo n.º 38
0
 def setup_catalog_cache(self, program_uuid, organization_key):
     """
     helper function to initialize a cached program with an single authoring_organization
     """
     catalog_org = CatalogOrganizationFactory.create(key=organization_key)
     program = ProgramFactory.create(
         uuid=program_uuid,
         authoring_organizations=[catalog_org]
     )
     cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=program_uuid), program, None)
Exemplo n.º 39
0
    def setUp(self):
        super(TestProgramMarketingDataExtender, self).setUp()

        # Ensure the E-Commerce service user exists
        UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True)

        self.course_price = 100
        self.number_of_courses = 2
        self.program = ProgramFactory(
            courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)]
        )
Exemplo n.º 40
0
    def test_get_programs_with_status_filtering(self, mock_get_edx_api_data):
        """ The function should request active and retired programs when the Waffle switch is enabled. """
        programs = ProgramFactory.create_batch(3)
        mock_get_edx_api_data.return_value = programs

        Switch.objects.get_or_create(name='display_retired_programs_on_learner_dashboard', defaults={'active': True})
        data = get_programs()

        expected_querystring = {
            'exclude_utm': 1,
            'status': ('active', 'retired',)
        }
        self.assert_contract(mock_get_edx_api_data.call_args, expected_querystring=expected_querystring)
        self.assertEqual(data, programs)
Exemplo n.º 41
0
    def setUp(self):
        super(ProgramPageBase, self).setUp()

        self.set_programs_api_configuration(is_enabled=True)

        self.programs = ProgramFactory.create_batch(3)
        self.pathways = PathwayFactory.create_batch(3)
        for pathway in self.pathways:
            self.programs += pathway['programs']

        # add some of the previously created programs to some pathways
        self.pathways[0]['programs'].extend([self.programs[0], self.programs[1]])
        self.pathways[1]['programs'].append(self.programs[0])

        self.username = None
Exemplo n.º 42
0
    def test_catalog_program_missing_org(self):
        """
        Test OrganizationDoesNotExistException is thrown if the cached program does not
        have an authoring organization.
        """
        program = ProgramFactory.create(
            uuid=self.program_uuid,
            authoring_organizations=[]
        )
        cache.set(PROGRAM_CACHE_KEY_TPL.format(uuid=self.program_uuid), program, None)

        organization = OrganizationFactory.create(short_name=self.organization_key)
        provider = SAMLProviderConfigFactory.create(organization=organization)
        self.create_social_auth_entry(self.user, provider, self.external_user_id)

        with pytest.raises(OrganizationDoesNotExistException):
            get_user_by_program_id(self.external_user_id, self.program_uuid)
    def setUp(self):
        super(TestCachePrograms, self).setUp()

        self.catalog_integration = self.create_catalog_integration()
        self.site_domain = 'testsite.com'
        self.set_up_site(
            self.site_domain,
            {
                'COURSE_CATALOG_API_URL': self.catalog_integration.get_internal_api_url().rstrip('/')
            }
        )

        self.list_url = self.catalog_integration.get_internal_api_url().rstrip('/') + '/programs/'
        self.detail_tpl = self.list_url.rstrip('/') + '/{uuid}/'

        self.programs = ProgramFactory.create_batch(3)
        self.uuids = [program['uuid'] for program in self.programs]
Exemplo n.º 44
0
    def test_get_programs_with_type(self, mock_get_program_types, mock_get_programs):
        """Verify get_programs_with_type returns the expected list of programs."""
        programs_with_program_type = []
        programs = ProgramFactory.create_batch(2)
        program_types = []

        for program in programs:
            program_type = ProgramTypeFactory(name=program['type'])
            program_types.append(program_type)

            program_with_type = copy.deepcopy(program)
            program_with_type['type'] = program_type
            programs_with_program_type.append(program_with_type)

        mock_get_programs.return_value = programs
        mock_get_program_types.return_value = program_types

        actual = get_programs_with_type(self.site)
        self.assertEqual(actual, programs_with_program_type)
Exemplo n.º 45
0
class TestProgramMarketingDataExtender(ModuleStoreTestCase):
    """Tests of the program data extender utility class."""
    ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT = '{root}/api/v2/baskets/calculate/'.format(root=ECOMMERCE_URL_ROOT)
    instructors = {
        'instructors': [
            {
                'name': 'test-instructor1',
                'organization': 'TextX',
            },
            {
                'name': 'test-instructor2',
                'organization': 'TextX',
            }
        ]
    }

    def setUp(self):
        super(TestProgramMarketingDataExtender, self).setUp()

        # Ensure the E-Commerce service user exists
        UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True)

        self.course_price = 100
        self.number_of_courses = 2
        self.program = ProgramFactory(
            courses=[_create_course(self, self.course_price) for __ in range(self.number_of_courses)],
            applicable_seat_types=['verified']
        )

    def _prepare_program_for_discounted_price_calculation_endpoint(self):
        """
        Program's applicable seat types should match some or all seat types of the seats that are a part of the program.
        Otherwise, ecommerce API endpoint for calculating the discounted price won't be called.

        Returns:
            seat: seat for which the discount is applicable
        """
        self.ecommerce_service = EcommerceService()
        seat = self.program['courses'][0]['course_runs'][0]['seats'][0]
        self.program['applicable_seat_types'] = [seat['type']]
        return seat

    def _update_discount_data(self, mock_discount_data):
        """
        Helper method that updates mocked discount data with
            - a flag indicating whether the program price is discounted
            - the amount of the discount (0 in case there's no discount)
        """
        program_discounted_price = mock_discount_data['total_incl_tax']
        program_full_price = mock_discount_data['total_incl_tax_excl_discounts']
        mock_discount_data.update({
            'is_discounted': program_discounted_price < program_full_price,
            'discount_value': program_full_price - program_discounted_price
        })

    def test_instructors(self):
        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.program.update(self.instructors['instructors'])
        self.assertEqual(data, self.program)

    def test_course_pricing(self):
        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        program_full_price = self.course_price * self.number_of_courses
        self.assertEqual(data['number_of_courses'], self.number_of_courses)
        self.assertEqual(data['full_program_price'], program_full_price)
        self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses)

    def test_course_pricing_when_all_course_runs_have_no_seats(self):
        # Create three seatless course runs and add them to the program
        course_runs = []
        for __ in range(3):
            course = ModuleStoreCourseFactory()
            course = self.update_course(course, self.user.id)
            course_runs.append(CourseRunFactory(key=unicode(course.id), seats=[]))
        program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)])

        data = ProgramMarketingDataExtender(program, self.user).extend()

        self.assertEqual(data['number_of_courses'], len(program['courses']))
        self.assertEqual(data['full_program_price'], 0.0)
        self.assertEqual(data['avg_price_per_course'], 0.0)

    @ddt.data(True, False)
    @mock.patch(UTILS_MODULE + '.has_access')
    def test_can_enroll(self, can_enroll, mock_has_access):
        """
        Verify that the student's can_enroll status is included.
        """
        mock_has_access.return_value = can_enroll

        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.assertEqual(data['courses'][0]['course_runs'][0]['can_enroll'], can_enroll)

    @httpretty.activate
    def test_fetching_program_discounted_price(self):
        """
        Authenticated users eligible for one click purchase should see the purchase button
            - displaying program's discounted price if it exists.
            - leading to ecommerce basket page
        """
        self._prepare_program_for_discounted_price_calculation_endpoint()
        mock_discount_data = {
            'total_incl_tax_excl_discounts': 200.0,
            'currency': 'USD',
            'total_incl_tax': 50.0
        }
        httpretty.register_uri(
            httpretty.GET,
            self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT,
            body=json.dumps(mock_discount_data),
            content_type='application/json'
        )

        data = ProgramMarketingDataExtender(self.program, self.user).extend()
        self._update_discount_data(mock_discount_data)

        self.assertEqual(
            data['skus'],
            [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']]
        )
        self.assertEqual(data['discount_data'], mock_discount_data)

    @httpretty.activate
    def test_fetching_program_discounted_price_as_anonymous_user(self):
        """
        Anonymous users should see the purchase button same way the authenticated users do
        when the program is eligible for one click purchase.
        """
        self._prepare_program_for_discounted_price_calculation_endpoint()
        mock_discount_data = {
            'total_incl_tax_excl_discounts': 200.0,
            'currency': 'USD',
            'total_incl_tax': 50.0
        }
        httpretty.register_uri(
            httpretty.GET,
            self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT,
            body=json.dumps(mock_discount_data),
            content_type='application/json'
        )

        data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend()
        self._update_discount_data(mock_discount_data)

        self.assertEqual(
            data['skus'],
            [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']]
        )
        self.assertEqual(data['discount_data'], mock_discount_data)

    def test_fetching_program_discounted_price_no_applicable_seats(self):
        """
        User shouldn't be able to do a one click purchase of a program if a program has no applicable seat types.
        """
        self.program['applicable_seat_types'] = []
        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.assertEqual(len(data['skus']), 0)

    @httpretty.activate
    def test_fetching_program_discounted_price_api_exception_caught(self):
        """
        User should be able to do a one click purchase of a program even if the ecommerce API throws an exception
        during the calculation of program discounted price.
        """
        self._prepare_program_for_discounted_price_calculation_endpoint()
        httpretty.register_uri(
            httpretty.GET,
            self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT,
            status=400,
            content_type='application/json'
        )

        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.assertEqual(
            data['skus'],
            [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']]
        )
Exemplo n.º 46
0
class TestProgramMarketingDataExtender(ModuleStoreTestCase):
    """Tests of the program data extender utility class."""
    instructors = {
        'instructors': [
            {
                'name': 'test-instructor1',
                'organization': 'TextX',
            },
            {
                'name': 'test-instructor2',
                'organization': 'TextX',
            }
        ]
    }

    def setUp(self):
        super(TestProgramMarketingDataExtender, self).setUp()

        self.course_price = 100
        self.number_of_courses = 2
        self.program = ProgramFactory(
            courses=[self._create_course(self.course_price) for __ in range(self.number_of_courses)]
        )

    def _create_course(self, course_price):
        """
        Creates the course in mongo and update it with the instructor data.
        Also creates catalog course with respect to course run.

        Returns:
            Catalog course dict.
        """
        course = ModuleStoreCourseFactory()
        course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
        course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
        course.instructor_info = self.instructors
        course = self.update_course(course, self.user.id)

        course_run = CourseRunFactory(
            key=unicode(course.id),
            seats=[SeatFactory(price=course_price)]
        )
        return CourseFactory(course_runs=[course_run])

    def test_instructors(self):
        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.program.update(self.instructors['instructors'])
        self.assertEqual(data, self.program)

    def test_course_pricing(self):
        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        program_full_price = self.course_price * self.number_of_courses
        self.assertEqual(data['number_of_courses'], self.number_of_courses)
        self.assertEqual(data['full_program_price'], program_full_price)
        self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses)

    @ddt.data(True, False)
    @mock.patch(UTILS_MODULE + '.has_access')
    def test_can_enroll(self, can_enroll, mock_has_access):
        """
        Verify that the student's can_enroll status is included.
        """
        mock_has_access.return_value = can_enroll

        data = ProgramMarketingDataExtender(self.program, self.user).extend()

        self.assertEqual(data['courses'][0]['course_runs'][0]['can_enroll'], can_enroll)
Exemplo n.º 47
0
    def test_get_many(self, mock_warning, mock_info):
        programs = ProgramFactory.create_batch(3)

        # Cache details for 2 of 3 programs.
        partial_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2]
        }
        cache.set_many(partial_programs, None)

        # When called before UUIDs are cached, the function should return an
        # empty list and log a warning.
        self.assertEqual(get_programs(self.site), [])
        mock_warning.assert_called_once_with('Failed to get program UUIDs from the cache.')
        mock_warning.reset_mock()

        # Cache UUIDs for all 3 programs.
        cache.set(
            SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain),
            [program['uuid'] for program in programs],
            None
        )

        actual_programs = get_programs(self.site)

        # The 2 cached programs should be returned while info and warning
        # messages should be logged for the missing one.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in partial_programs.values())
        )
        mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.')
        mock_warning.assert_called_with(
            'Failed to get details for program {uuid} from the cache.'.format(uuid=programs[2]['uuid'])
        )
        mock_warning.reset_mock()

        # We can't use a set comparison here because these values are dictionaries
        # and aren't hashable. We've already verified that all programs came out
        # of the cache above, so all we need to do here is verify the accuracy of
        # the data itself.
        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, partial_programs[key])

        # Cache details for all 3 programs.
        all_programs = {
            PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs
        }
        cache.set_many(all_programs, None)

        actual_programs = get_programs(self.site)

        # All 3 programs should be returned.
        self.assertEqual(
            set(program['uuid'] for program in actual_programs),
            set(program['uuid'] for program in all_programs.values())
        )
        self.assertFalse(mock_warning.called)

        for program in actual_programs:
            key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid'])
            self.assertEqual(program, all_programs[key])