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: +
+ ## Translators: This message appears to users when the users have not completed identity verification. +

${_("Certificate unavailable")}

+

${_("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.")}

+
+
%else:

${_("Congratulations, you qualified for a certificate!")}

diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html index a07acf823f..4ec7e965db 100644 --- a/lms/templates/dashboard/_dashboard_certificate_information.html +++ b/lms/templates/dashboard/_dashboard_certificate_information.html @@ -1,4 +1,4 @@ -<%page expression_filter="h" args="cert_status, course_overview, enrollment" /> +<%page expression_filter="h" args="cert_status, course_overview, enrollment, reverify_link" /> <%! from django.utils.translation import ugettext as _ @@ -30,7 +30,7 @@ else:
% if cert_status['status'] == 'processing':

${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}

-% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'): +% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'):

${_("Your final grade:")} ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. % if cert_status['status'] == 'notpassing': @@ -48,6 +48,11 @@ else:

${Text(_("Your {cert_name_long} is being held pending confirmation that the issuance of your {cert_name_short} is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}.")).format(email=HTML('{email}.').format(email=settings.CONTACT_EMAIL), cert_name_short=cert_name_short, cert_name_long=cert_name_long)}

+ % elif cert_status['status'] == 'unverified': +

+ ${Text(_("Your certificate was not issued because you do not have a current verified identity with {platform_name}. ")).format(platform_name=settings.PLATFORM_NAME)} + ${Text(_("Verify your identity now."))} +

% endif

% endif diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 38f503a101..0f292429e7 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -21,6 +21,7 @@ from student.helpers import ( %> <% + reverify_link = reverse('verify_student_reverify') cert_name_short = course_overview.cert_name_short if cert_name_short == "": cert_name_short = settings.CERT_NAME_SHORT @@ -280,7 +281,7 @@ from student.helpers import (