diff --git a/common/djangoapps/student/tests/test_certificates.py b/common/djangoapps/student/tests/test_certificates.py index 711fa6e563..2a8203198b 100644 --- a/common/djangoapps/student/tests/test_certificates.py +++ b/common/djangoapps/student/tests/test_certificates.py @@ -15,6 +15,7 @@ from xmodule.modulestore.tests.factories import CourseFactory from student.tests.factories import UserFactory, CourseEnrollmentFactory from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error from certificates.api import get_certificate_url # pylint: disable=import-error +from certificates.models import CertificateStatuses # pylint: disable=import-error from course_modes.models import CourseMode from student.models import LinkedInAddToProfileConfiguration @@ -111,6 +112,17 @@ class CertificateDisplayTest(SharedModuleStoreTestCase): self.assertContains(response, u'View Test_Certificate') self.assertContains(response, test_url) + @ddt.data('verified', 'honor', 'professional') + def test_unverified_certificate_message(self, enrollment_mode): + cert = self._create_certificate(enrollment_mode) + cert.status = CertificateStatuses.unverified + cert.save() + response = self.client.get(reverse('dashboard')) + self.assertContains( + response, + u'do not have a current verified identity with {platform_name}' + .format(platform_name=settings.PLATFORM_NAME)) + def test_post_to_linkedin_invisibility(self): """ Verifies that the post certificate to linked button diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index cb71bf5163..2b5cfbe90e 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -319,6 +319,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa CertificateStatuses.auditing: 'auditing', CertificateStatuses.audit_passing: 'auditing', CertificateStatuses.audit_notpassing: 'auditing', + CertificateStatuses.unverified: 'unverified', } default_status = 'processing' @@ -350,7 +351,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa 'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES, } - if (status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing') and + if (status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified') and course_overview.end_of_course_survey_url is not None): status_dict.update({ 'show_survey_button': True, @@ -394,7 +395,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa cert_status['download_url'] ) - if status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'): + if status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'): if 'grade' not in cert_status: # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x, # who need to be regraded (we weren't tracking 'notpassing' at first). diff --git a/lms/djangoapps/ccx/tests/test_field_override_performance.py b/lms/djangoapps/ccx/tests/test_field_override_performance.py index 82f8f3467f..d978f8626e 100644 --- a/lms/djangoapps/ccx/tests/test_field_override_performance.py +++ b/lms/djangoapps/ccx/tests/test_field_override_performance.py @@ -231,21 +231,21 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): # # of mongo queries, # # of xblocks # ) - ('no_overrides', 1, True, False): (47, 1, 6, 13), - ('no_overrides', 2, True, False): (119, 16, 6, 84), - ('no_overrides', 3, True, False): (399, 81, 6, 335), - ('ccx', 1, True, False): (47, 1, 6, 13), - ('ccx', 2, True, False): (119, 16, 6, 84), - ('ccx', 3, True, False): (399, 81, 6, 335), + ('no_overrides', 1, True, False): (48, 1, 6, 13), + ('no_overrides', 2, True, False): (120, 16, 6, 84), + ('no_overrides', 3, True, False): (400, 81, 6, 335), + ('ccx', 1, True, False): (48, 1, 6, 13), + ('ccx', 2, True, False): (120, 16, 6, 84), + ('ccx', 3, True, False): (400, 81, 6, 335), ('ccx', 1, True, True): (47, 1, 6, 13), ('ccx', 2, True, True): (119, 16, 6, 84), ('ccx', 3, True, True): (399, 81, 6, 335), - ('no_overrides', 1, False, False): (47, 1, 6, 13), - ('no_overrides', 2, False, False): (119, 16, 6, 84), - ('no_overrides', 3, False, False): (399, 81, 6, 335), - ('ccx', 1, False, False): (47, 1, 6, 13), - ('ccx', 2, False, False): (119, 16, 6, 84), - ('ccx', 3, False, False): (399, 81, 6, 335), + ('no_overrides', 1, False, False): (48, 1, 6, 13), + ('no_overrides', 2, False, False): (120, 16, 6, 84), + ('no_overrides', 3, False, False): (400, 81, 6, 335), + ('ccx', 1, False, False): (48, 1, 6, 13), + ('ccx', 2, False, False): (120, 16, 6, 84), + ('ccx', 3, False, False): (400, 81, 6, 335), ('ccx', 1, False, True): (47, 1, 6, 13), ('ccx', 2, False, True): (119, 16, 6, 84), ('ccx', 3, False, True): (399, 81, 6, 335), @@ -260,22 +260,22 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): __test__ = True TEST_DATA = { - ('no_overrides', 1, True, False): (47, 1, 4, 9), - ('no_overrides', 2, True, False): (119, 16, 19, 54), - ('no_overrides', 3, True, False): (399, 81, 84, 215), - ('ccx', 1, True, False): (47, 1, 4, 9), - ('ccx', 2, True, False): (119, 16, 19, 54), - ('ccx', 3, True, False): (399, 81, 84, 215), - ('ccx', 1, True, True): (49, 1, 4, 13), - ('ccx', 2, True, True): (121, 16, 19, 84), - ('ccx', 3, True, True): (401, 81, 84, 335), - ('no_overrides', 1, False, False): (47, 1, 4, 9), - ('no_overrides', 2, False, False): (119, 16, 19, 54), - ('no_overrides', 3, False, False): (399, 81, 84, 215), - ('ccx', 1, False, False): (47, 1, 4, 9), - ('ccx', 2, False, False): (119, 16, 19, 54), - ('ccx', 3, False, False): (399, 81, 84, 215), + ('no_overrides', 1, True, False): (48, 1, 4, 9), + ('no_overrides', 2, True, False): (120, 16, 19, 54), + ('no_overrides', 3, True, False): (400, 81, 84, 215), + ('ccx', 1, True, False): (48, 1, 4, 9), + ('ccx', 2, True, False): (120, 16, 19, 54), + ('ccx', 3, True, False): (400, 81, 84, 215), + ('ccx', 1, True, True): (50, 1, 4, 13), + ('ccx', 2, True, True): (122, 16, 19, 84), + ('ccx', 3, True, True): (402, 81, 84, 335), + ('no_overrides', 1, False, False): (48, 1, 4, 9), + ('no_overrides', 2, False, False): (120, 16, 19, 54), + ('no_overrides', 3, False, False): (400, 81, 84, 215), + ('ccx', 1, False, False): (48, 1, 4, 9), + ('ccx', 2, False, False): (120, 16, 19, 54), + ('ccx', 3, False, False): (400, 81, 84, 215), ('ccx', 1, False, True): (47, 1, 4, 9), ('ccx', 2, False, True): (119, 16, 19, 54), - ('ccx', 3, False, True): (399, 81, 84, 215), + ('ccx', 3, False, True): (400, 81, 84, 215), } diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index e9f31172ff..774647f295 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -225,6 +225,7 @@ def certificate_downloadable_status(student, course_key): 'is_downloadable': False, 'is_generating': True if current_status['status'] in [CertificateStatuses.generating, CertificateStatuses.error] else False, + 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, 'download_url': None, 'uuid': None, } diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 17031810c9..7fad232e0e 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -88,6 +88,7 @@ class CertificateStatuses(object): auditing = 'auditing' audit_passing = 'audit_passing' audit_notpassing = 'audit_notpassing' + unverified = 'unverified' readable_statuses = { downloadable: "already received", @@ -466,6 +467,9 @@ def certificate_status_for_student(student, course_id): should not be issued a certificate. This will be set if allow_certificate is set to False in the userprofile table + unverified - The student is in verified enrollment track and + the student did not have their identity verified, + even though they should be eligible for the cert otherwise. If the status is "downloadable", the dictionary also contains "download_url". diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index 40bae9af47..94626ac235 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -263,7 +263,7 @@ class XQueueCertInterface(object): user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode) - + unverified = False # For credit mode generate verified certificate if cert_mode == CourseMode.CREDIT_MODE: cert_mode = CourseMode.VERIFIED @@ -274,7 +274,10 @@ class XQueueCertInterface(object): template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) - cert_mode = GeneratedCertificate.MODES.honor + if CourseMode.mode_for_course(course_id, CourseMode.HONOR): + cert_mode = GeneratedCertificate.MODES.honor + else: + unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) @@ -388,6 +391,20 @@ class XQueueCertInterface(object): ) return cert + if unverified: + cert.status = status.unverified + cert.save() + LOGGER.info( + ( + u"User %s has a verified enrollment in course %s " + u"but is missing ID verification. " + u"Certificate status has been set to unverified" + ), + student.id, + unicode(course_id), + ) + return cert + # Finally, generate the certificate and send it off. return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf) diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index b76be2add9..dc65f50ffe 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -117,6 +117,7 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes { 'is_downloadable': False, 'is_generating': True, + 'is_unverified': False, 'download_url': None, 'uuid': None, } @@ -135,6 +136,7 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes { 'is_downloadable': False, 'is_generating': True, + 'is_unverified': False, 'download_url': None, 'uuid': None } @@ -146,6 +148,7 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes { 'is_downloadable': False, 'is_generating': False, + 'is_unverified': False, 'download_url': None, 'uuid': None, } @@ -169,6 +172,7 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes { 'is_downloadable': True, 'is_generating': False, + 'is_unverified': False, 'download_url': 'www.google.com', 'uuid': cert.verify_uuid } @@ -194,6 +198,7 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes { 'is_downloadable': True, 'is_generating': False, + 'is_unverified': False, 'download_url': '/certificates/user/{user_id}/course/{course_id}'.format( user_id=self.student.id, # pylint: disable=no-member course_id=self.course.id, diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 741b4c0eff..e87c5441de 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -1367,7 +1367,7 @@ class ProgressPageTests(ModuleStoreTestCase): self.assertContains(resp, u"Download Your Certificate") @ddt.data( - *itertools.product(((41, 4, True), (41, 4, False)), (True, False)) + *itertools.product(((42, 4, True), (42, 4, False)), (True, False)) ) @ddt.unpack def test_query_counts(self, (sql_calls, mongo_calls, self_paced), self_paced_enabled): @@ -1382,22 +1382,32 @@ class ProgressPageTests(ModuleStoreTestCase): 'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': [] })) @ddt.data( - (CourseMode.AUDIT, False), - (CourseMode.HONOR, True), - (CourseMode.VERIFIED, True), - (CourseMode.PROFESSIONAL, True), - (CourseMode.NO_ID_PROFESSIONAL_MODE, True), - (CourseMode.CREDIT_MODE, True), + *itertools.product( + ( + CourseMode.AUDIT, + CourseMode.HONOR, + CourseMode.VERIFIED, + CourseMode.PROFESSIONAL, + CourseMode.NO_ID_PROFESSIONAL_MODE, + CourseMode.CREDIT_MODE + ), + (True, False) + ) ) @ddt.unpack - def test_show_certificate_request_button(self, course_mode, show_button): + def test_show_certificate_request_button(self, course_mode, user_verified): """Verify that the Request Certificate is not displayed in audit mode.""" CertificateGenerationConfiguration(enabled=True).save() certs_api.set_cert_generation_enabled(self.course.id, True) CourseEnrollment.enroll(self.user, self.course.id, mode=course_mode) - - resp = views.progress(self.request, course_id=unicode(self.course.id)) - self.assertEqual(show_button, 'Request Certificate' in resp.content) + with patch( + 'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified' + ) as user_verify: + user_verify.return_value = user_verified + resp = views.progress(self.request, course_id=unicode(self.course.id)) + self.assertEqual( + course_mode is not CourseMode.AUDIT and user_verified, + 'Request Certificate' in resp.content) @attr('shard_1') diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 501b816257..adbf0e0013 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -66,6 +66,7 @@ from courseware.url_helpers import get_redirect_url, get_redirect_url_for_global from courseware.user_state_client import DjangoXBlockUserStateClient from edxmako.shortcuts import render_to_response, render_to_string, marketing_link from instructor.enrollment import uses_shib +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.coursetalk.helpers import inject_coursetalk_keys_into_context from openedx.core.djangoapps.credit.api import ( @@ -736,6 +737,7 @@ def _progress(request, course_key, student_id): 'passed': is_course_passed(course, grade_summary), 'show_generate_cert_btn': show_generate_cert_btn, 'credit_course_requirements': _credit_course_requirements(course_key, student), + 'is_id_verified': SoftwareSecurePhotoVerification.user_is_verified(student) } if show_generate_cert_btn: diff --git a/lms/templates/courseware/progress.html b/lms/templates/courseware/progress.html index 522adfa7cb..dd7632ca2b 100644 --- a/lms/templates/courseware/progress.html +++ b/lms/templates/courseware/progress.html @@ -80,6 +80,13 @@ from django.utils.http import urlquote_plus
${_("We're creating your certificate. You can keep working in your courses and a link to it will appear here and on your Dashboard when it is ready.")}
+ %elif not is_id_verified or is_unverified: +${_("You have not received a certificate because you do not have a current {platform_name} verified identity. ").format(platform_name=settings.PLATFORM_NAME)} ${_("Verify your identity now.")}
+