diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 41d493f8860798fff48740f52fc8b6628d5f54cf..592fb542545ef948ff6155d6266175c3c2973394 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -66,6 +66,23 @@ class TestProgramProgressMeter(TestCase): for program in programs: program['detail_url'] = reverse('program_details_view', kwargs={'program_uuid': program['uuid']}) + def _make_certificate_result(self, **kwargs): + """Helper to create dummy results from the certificates API.""" + result = { + 'username': 'dummy-username', + 'course_key': 'dummy-course', + 'type': 'dummy-type', + 'status': 'dummy-status', + 'download_url': 'http://www.example.com/cert.pdf', + 'grade': '0.98', + 'created': '2015-07-31T00:00:00Z', + 'modified': '2015-07-31T00:00:00Z', + } + + result.update(**kwargs) + + return result + def test_no_enrollments(self, mock_get_programs): """Verify behavior when programs exist, but no relevant enrollments do.""" data = [ProgramFactory()] @@ -453,54 +470,15 @@ class TestProgramProgressMeter(TestCase): meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, program_uuids) - @mock.patch(UTILS_MODULE + '.ProgramProgressMeter.completed_course_runs', new_callable=mock.PropertyMock) - 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']]) - @mock.patch(UTILS_MODULE + '.certificate_api.get_certificates_for_user') def test_completed_course_runs(self, mock_get_certificates_for_user, _mock_get_programs): """ Verify that the method can find course run certificates when not mocked out. """ - def make_certificate_result(**kwargs): - """Helper to create dummy results from the certificates API.""" - result = { - 'username': 'dummy-username', - 'course_key': 'dummy-course', - 'type': 'dummy-type', - 'status': 'dummy-status', - 'download_url': 'http://www.example.com/cert.pdf', - 'grade': '0.98', - 'created': '2015-07-31T00:00:00Z', - 'modified': '2015-07-31T00:00:00Z', - } - result.update(**kwargs) - return result - mock_get_certificates_for_user.return_value = [ - make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'), - make_certificate_result(status='generating', type='honor', course_key='generating-course'), - make_certificate_result(status='unknown', course_key='unknown-course'), + self._make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'), + self._make_certificate_result(status='generating', type='honor', course_key='generating-course'), + self._make_certificate_result(status='unknown', course_key='unknown-course'), ] meter = ProgramProgressMeter(self.user) @@ -513,6 +491,32 @@ class TestProgramProgressMeter(TestCase): ) mock_get_certificates_for_user.assert_called_with(self.user.username) + @mock.patch(UTILS_MODULE + '.certificate_api.get_certificates_for_user') + 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.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.user) + self.assertEqual(meter.completed_programs, [program['uuid']]) + @ddt.ddt @override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT) diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index ec4b99125f69deae844a2dcdba1bd68073c67eff..01a151b032522fd71f818a0edd109ac81c0e2845 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -260,12 +260,6 @@ class ProgramProgressMeter(object): Modify the structure of a course run dict to facilitate comparison with course run certificates. """ - course_run_type = course_run['type'] - - # Treat no-id-professional enrollments as professional - if course_run_type == CourseMode.NO_ID_PROFESSIONAL_MODE: - course_run_type = CourseMode.PROFESSIONAL - return { 'course_run_id': course_run['key'], # A course run's type is assumed to indicate which mode must be @@ -275,7 +269,7 @@ class ProgramProgressMeter(object): # count towards completion of a course in a program). This may change # in the future to make use of the more rigid set of "applicable seat # types" associated with each program type in the catalog. - 'type': course_run_type, + 'type': course_run['type'], } return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs']) @@ -309,16 +303,25 @@ class ProgramProgressMeter(object): dict with a list of completed and failed runs """ course_run_certificates = certificate_api.get_certificates_for_user(self.user.username) + completed_runs, failed_runs = [], [] for certificate in course_run_certificates: + certificate_type = certificate['type'] + + # Treat "no-id-professional" certificates as "professional" certificates + if certificate_type == CourseMode.NO_ID_PROFESSIONAL_MODE: + certificate_type = CourseMode.PROFESSIONAL + course_data = { 'course_run_id': unicode(certificate['course_key']), - 'type': certificate['type'] + 'type': certificate_type } + if certificate_api.is_passing_status(certificate['status']): completed_runs.append(course_data) else: failed_runs.append(course_data) + return {'completed': completed_runs, 'failed': failed_runs} def _is_course_enrolled(self, course):