Commit 70e13407 by Renzo Lucioni

Fix course completion logic when determining program completion

We want to treat professional certificates equally for the purposes of program completion.

LEARNER-601
parent 4c3c13c2
...@@ -66,6 +66,23 @@ class TestProgramProgressMeter(TestCase): ...@@ -66,6 +66,23 @@ class TestProgramProgressMeter(TestCase):
for program in programs: for program in programs:
program['detail_url'] = reverse('program_details_view', kwargs={'program_uuid': program['uuid']}) 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): def test_no_enrollments(self, mock_get_programs):
"""Verify behavior when programs exist, but no relevant enrollments do.""" """Verify behavior when programs exist, but no relevant enrollments do."""
data = [ProgramFactory()] data = [ProgramFactory()]
...@@ -453,54 +470,15 @@ class TestProgramProgressMeter(TestCase): ...@@ -453,54 +470,15 @@ class TestProgramProgressMeter(TestCase):
meter = ProgramProgressMeter(self.user) meter = ProgramProgressMeter(self.user)
self.assertEqual(meter.completed_programs, program_uuids) 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') @mock.patch(UTILS_MODULE + '.certificate_api.get_certificates_for_user')
def test_completed_course_runs(self, mock_get_certificates_for_user, _mock_get_programs): 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. 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 = [ mock_get_certificates_for_user.return_value = [
make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'), self._make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'),
make_certificate_result(status='generating', type='honor', course_key='generating-course'), self._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='unknown', course_key='unknown-course'),
] ]
meter = ProgramProgressMeter(self.user) meter = ProgramProgressMeter(self.user)
...@@ -513,6 +491,32 @@ class TestProgramProgressMeter(TestCase): ...@@ -513,6 +491,32 @@ class TestProgramProgressMeter(TestCase):
) )
mock_get_certificates_for_user.assert_called_with(self.user.username) 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 @ddt.ddt
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT) @override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT)
......
...@@ -260,12 +260,6 @@ class ProgramProgressMeter(object): ...@@ -260,12 +260,6 @@ class ProgramProgressMeter(object):
Modify the structure of a course run dict to facilitate comparison Modify the structure of a course run dict to facilitate comparison
with course run certificates. 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 { return {
'course_run_id': course_run['key'], 'course_run_id': course_run['key'],
# A course run's type is assumed to indicate which mode must be # A course run's type is assumed to indicate which mode must be
...@@ -275,7 +269,7 @@ class ProgramProgressMeter(object): ...@@ -275,7 +269,7 @@ class ProgramProgressMeter(object):
# count towards completion of a course in a program). This may change # 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 # in the future to make use of the more rigid set of "applicable seat
# types" associated with each program type in the catalog. # 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']) return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])
...@@ -309,16 +303,25 @@ class ProgramProgressMeter(object): ...@@ -309,16 +303,25 @@ class ProgramProgressMeter(object):
dict with a list of completed and failed runs dict with a list of completed and failed runs
""" """
course_run_certificates = certificate_api.get_certificates_for_user(self.user.username) course_run_certificates = certificate_api.get_certificates_for_user(self.user.username)
completed_runs, failed_runs = [], [] completed_runs, failed_runs = [], []
for certificate in course_run_certificates: 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_data = {
'course_run_id': unicode(certificate['course_key']), 'course_run_id': unicode(certificate['course_key']),
'type': certificate['type'] 'type': certificate_type
} }
if certificate_api.is_passing_status(certificate['status']): if certificate_api.is_passing_status(certificate['status']):
completed_runs.append(course_data) completed_runs.append(course_data)
else: else:
failed_runs.append(course_data) failed_runs.append(course_data)
return {'completed': completed_runs, 'failed': failed_runs} return {'completed': completed_runs, 'failed': failed_runs}
def _is_course_enrolled(self, course): def _is_course_enrolled(self, course):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment