diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index c2645fdbee3c..520958b47305 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -687,16 +687,18 @@ def min_course_price_for_currency(cls, course_id, currency): def is_eligible_for_certificate(cls, mode_slug): """ Returns whether or not the given mode_slug is eligible for a - certificate. Currently all modes other than 'audit' and `honor` - grant a certificate. Note that audit enrollments which existed - prior to December 2015 *were* given certificates, so there will - be GeneratedCertificate records with mode='audit' which are + certificate. Currently all modes other than 'audit' grant a + certificate. Note that audit enrollments which existed prior + to December 2015 *were* given certificates, so there will be + GeneratedCertificate records with mode='audit' which are eligible. """ - if mode_slug == cls.AUDIT or mode_slug == cls.HONOR: - return False + ineligible_modes = [cls.AUDIT] + + if settings.FEATURES['DISABLE_HONOR_CERTIFICATES']: + ineligible_modes.append(cls.HONOR) - return True + return mode_slug not in ineligible_modes def to_tuple(self): """ diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 46062cca5deb..7cb2e28ed0c6 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -455,17 +455,24 @@ def test_expiration_datetime_explicitly_set_to_none(self): self.assertIsNone(verified_mode.expiration_datetime) @ddt.data( - (CourseMode.AUDIT, False), - (CourseMode.HONOR, False), - (CourseMode.VERIFIED, True), - (CourseMode.CREDIT_MODE, True), - (CourseMode.PROFESSIONAL, True), - (CourseMode.NO_ID_PROFESSIONAL_MODE, True), + (False, CourseMode.AUDIT, False), + (False, CourseMode.HONOR, True), + (False, CourseMode.VERIFIED, True), + (False, CourseMode.CREDIT_MODE, True), + (False, CourseMode.PROFESSIONAL, True), + (False, CourseMode.NO_ID_PROFESSIONAL_MODE, True), + (True, CourseMode.AUDIT, False), + (True, CourseMode.HONOR, False), + (True, CourseMode.VERIFIED, True), + (True, CourseMode.CREDIT_MODE, True), + (True, CourseMode.PROFESSIONAL, True), + (True, CourseMode.NO_ID_PROFESSIONAL_MODE, True), ) @ddt.unpack - def test_eligible_for_cert(self, mode_slug, expected_eligibility): + def test_eligible_for_cert(self, disable_honor_cert, mode_slug, expected_eligibility): """Verify that non-audit modes are eligible for a cert.""" - self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility) + with override_settings(FEATURES={'DISABLE_HONOR_CERTIFICATES': disable_honor_cert}): + self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility) @ddt.data( (CourseMode.AUDIT, False), diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 6bc6c9256e68..d9dcc6dc6ad7 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -93,6 +93,9 @@ QUERY_COUNT_TABLE_BLACKLIST = WAFFLE_TABLES +FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy() +FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True + @attr(shard=5) class TestJumpTo(ModuleStoreTestCase): @@ -1351,7 +1354,7 @@ def test_view_certificate_link(self): course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", - mode='verified' + mode='honor' ) # Enable the feature, but do not enable it for this course @@ -1377,34 +1380,29 @@ def test_view_certificate_link(self): self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - resp = self._get_progress_page() + resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") + self.assertContains(resp, u"View Certificate") - self.assertContains(resp, u"earned a certificate for this course") - cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid) - self.assertContains(resp, cert_url) + self.assertContains(resp, u"earned a certificate for this course") + cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid) + self.assertContains(resp, cert_url) - # when course certificate is not active - certificates[0]['is_active'] = False - self.store.update_item(self.course, self.user.id) + # when course certificate is not active + certificates[0]['is_active'] = False + self.store.update_item(self.course, self.user.id) - resp = self._get_progress_page() - self.assertNotContains(resp, u"View Your Certificate") - self.assertNotContains(resp, u"You can now view your certificate") - self.assertContains(resp, "Your certificate is available") - self.assertContains(resp, "earned a certificate for this course.") + resp = self._get_progress_page() + self.assertNotContains(resp, u"View Your Certificate") + self.assertNotContains(resp, u"You can now view your certificate") + self.assertContains(resp, "Your certificate is available") + self.assertContains(resp, "earned a certificate for this course.") @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False}) @@ -1418,7 +1416,7 @@ def test_view_certificate_link_hidden(self): course_id=self.course.id, status=CertificateStatuses.downloadable, download_url="http://www.example.com/certificate.pdf", - mode='verified' + mode='honor' ) # Enable the feature, but do not enable it for this course @@ -1427,19 +1425,13 @@ def test_view_certificate_link_hidden(self): # Enable certificate generation for this course certs_api.set_cert_generation_enabled(self.course.id, True) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - resp = self._get_progress_page() - self.assertContains(resp, u"Download Your Certificate") + resp = self._get_progress_page() + self.assertContains(resp, u"Download Your Certificate") @ddt.data( (True, 56), @@ -1509,7 +1501,7 @@ def test_show_certificate_request_button(self, course_mode, user_verified): resp = self._get_progress_page() - cert_button_hidden = course_mode in (CourseMode.AUDIT, CourseMode.HONOR) or \ + cert_button_hidden = course_mode is CourseMode.AUDIT or \ course_mode in CourseMode.VERIFIED_MODES and not user_verified self.assertEqual( @@ -1524,7 +1516,7 @@ def test_page_with_invalidated_certificate_with_html_view(self): re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations @@ -1543,22 +1535,17 @@ def test_page_with_invalidated_certificate_with_html_view(self): self.course.cert_html_view_enabled = True self.course.save() self.store.update_item(self.course, self.user.id) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = { - 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} - } + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = True + course_grade.summary = { + 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} + } - resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") - self.assert_invalidate_certificate(generated_certificate) + resp = self._get_progress_page() + self.assertContains(resp, u"View Certificate") + self.assert_invalidate_certificate(generated_certificate) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) def test_page_with_whitelisted_certificate_with_html_view(self): @@ -1567,7 +1554,7 @@ def test_page_with_whitelisted_certificate_with_html_view(self): appearing on dashboard """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) # Course certificate configurations @@ -1591,22 +1578,17 @@ def test_page_with_whitelisted_certificate_with_html_view(self): course_id=self.course.id, whitelist=True ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = False - course_grade.summary = { - 'grade': 'Fail', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} - } + with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: + course_grade = mock_create.return_value + course_grade.passed = False + course_grade.summary = { + 'grade': 'Fail', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {} + } - resp = self._get_progress_page() - self.assertContains(resp, u"View Certificate") - self.assert_invalidate_certificate(generated_certificate) + resp = self._get_progress_page() + self.assertContains(resp, u"View Certificate") + self.assert_invalidate_certificate(generated_certificate) @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) def test_page_with_invalidated_certificate_with_pdf(self): @@ -1615,44 +1597,17 @@ def test_page_with_invalidated_certificate_with_pdf(self): re-generate button should not appear on progress page. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: - course_grade = mock_create.return_value - course_grade.passed = True - course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - - resp = self._get_progress_page() - self.assertContains(resp, u'Download Your Certificate') - self.assert_invalidate_certificate(generated_certificate) - - @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) - @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) - def test_message_for_audit_mode(self): - """ Verify that message appears on progress page, if learner is enrolled - in audit mode. - """ - user = UserFactory.create() - self.assertTrue(self.client.login(username=user.username, password='test')) - CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value course_grade.passed = True course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}} - response = self._get_progress_page() - - self.assertContains( - response, - u'You are enrolled in the audit track for this course. The audit track does not include a certificate.' - ) + resp = self._get_progress_page() + self.assertContains(resp, u'Download Your Certificate') + self.assert_invalidate_certificate(generated_certificate) @ddt.data( *itertools.product( @@ -1719,13 +1674,15 @@ def test_progress_without_course_duration_limits(self, course_mode): @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) @patch('lms.djangoapps.certificates.api.get_active_web_certificate', PropertyMock(return_value=True)) - def test_message_for_honor_mode(self): + @override_settings(FEATURES=FEATURES_WITH_DISABLE_HONOR_CERTIFICATE) + @ddt.data(CourseMode.AUDIT, CourseMode.HONOR) + def test_message_for_ineligible_mode(self, course_mode): """ Verify that message appears on progress page, if learner is enrolled - in honor mode. + in an ineligible mode. """ user = UserFactory.create() self.assertTrue(self.client.login(username=user.username, password='test')) - CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.HONOR) + CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=course_mode) with patch('lms.djangoapps.grades.course_grade_factory.CourseGradeFactory.read') as mock_create: course_grade = mock_create.return_value @@ -1734,17 +1691,16 @@ def test_message_for_honor_mode(self): response = self._get_progress_page() - self.assertContains( - response, - u'You are enrolled in the honor track for this course. The honor track does not include a certificate.' - ) + expected_message = (u'You are enrolled in the {mode} track for this course. ' + u'The {mode} track does not include a certificate.').format(mode=course_mode) + self.assertContains(response, expected_message) def test_invalidated_cert_data(self): """ Verify that invalidated cert data is returned if cert is invalidated. """ generated_certificate = self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) CertificateInvalidationFactory.create( @@ -1753,7 +1709,7 @@ def test_invalidated_cert_data(self): ) # Invalidate user certificate generated_certificate.invalidate() - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'invalidated') self.assertEqual(response.title, 'Your certificate has been invalidated') @@ -1762,17 +1718,11 @@ def test_downloadable_get_cert_data(self): Verify that downloadable cert data is returned if cert is downloadable. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - - with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', - return_value=self.mock_certificate_downloadable_status(is_downloadable=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', + return_value=self.mock_certificate_downloadable_status(is_downloadable=True)): + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'downloadable') self.assertEqual(response.title, 'Your certificate is available') @@ -1782,11 +1732,11 @@ def test_generating_get_cert_data(self): Verify that generating cert data is returned if cert is generating. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_generating=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'generating') self.assertEqual(response.title, "We're working on it...") @@ -1796,11 +1746,11 @@ def test_unverified_get_cert_data(self): Verify that unverified cert data is returned if cert is unverified. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', return_value=self.mock_certificate_downloadable_status(is_unverified=True)): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'unverified') self.assertEqual(response.title, "Certificate unavailable") @@ -1810,16 +1760,11 @@ def test_request_get_cert_data(self): Verify that requested cert data is returned if cert is to be requested. """ self.generate_certificate( - "http://www.example.com/certificate.pdf", "verified" + "http://www.example.com/certificate.pdf", "honor" ) - CourseEnrollment.enroll(self.user, self.course.id, mode="verified") - with patch( - 'lms.djangoapps.verify_student.services.IDVerificationService.user_is_verified' - ) as user_verify: - user_verify.return_value = True - with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', - return_value=self.mock_certificate_downloadable_status()): - response = views._get_cert_data(self.user, self.course, CourseMode.VERIFIED, MagicMock(passed=True)) + with patch('lms.djangoapps.certificates.api.certificate_downloadable_status', + return_value=self.mock_certificate_downloadable_status()): + response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True)) self.assertEqual(response.cert_status, 'requesting') self.assertEqual(response.title, "Congratulations, you qualified for a certificate!") diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 73dabbce09e2..923f180d959a 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -143,6 +143,11 @@ cert_web_view_url=None ) +INELIGIBLE_PASSING_CERT_DATA = { + CourseMode.AUDIT: AUDIT_PASSING_CERT_DATA, + CourseMode.HONOR: HONOR_PASSING_CERT_DATA +} + GENERATING_CERT_DATA = CertData( CertificateStatuses.generating, _("We're working on it..."), @@ -1075,7 +1080,7 @@ def _get_cert_data(student, course, enrollment_mode, course_grade=None): returns dict if course certificate is available else None. """ if not CourseMode.is_eligible_for_certificate(enrollment_mode): - return AUDIT_PASSING_CERT_DATA if enrollment_mode == CourseMode.AUDIT else HONOR_PASSING_CERT_DATA + return INELIGIBLE_PASSING_CERT_DATA.get(enrollment_mode) certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id) if course_grade is None: diff --git a/lms/envs/common.py b/lms/envs/common.py index 919f90c6e070..d3b1231bf346 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -183,6 +183,18 @@ # Toggle to enable certificates of courses on dashboard 'ENABLE_VERIFIED_CERTIFICATES': False, + # .. toggle_name: DISABLE_HONOR_CERTIFICATES + # .. toggle_type: feature_flag + # .. toggle_default: False + # .. toggle_description: Set to True to disable honor certificates. Typically used when your installation only allows verified certificates, like courses.edx.org. + # .. toggle_category: certificates + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2019-05-14 + # .. toggle_expiration_date: None + # .. toggle_tickets: https://openedx.atlassian.net/browse/PROD-269 + # .. toggle_status: supported + 'DISABLE_HONOR_CERTIFICATES': False, # Toggle to disable honor certificates + # for acceptance and load testing 'AUTOMATIC_AUTH_FOR_TESTING': False,