Commit 6f983c33 by Simon Chen

Merge pull request #12484 from edx/schen/ECOM-4007

ECOM-4007 Add new cert status "unverified" to handle no active user ID verification
parents 18cddf6e e6137dd6
......@@ -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
......
......@@ -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).
......
......@@ -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),
}
......@@ -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,
}
......
......@@ -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".
......
......@@ -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)
......
......@@ -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,
......
......@@ -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')
......
......@@ -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:
......
......@@ -80,6 +80,13 @@ from django.utils.http import urlquote_plus
<p class="copy">${_("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.")}</p>
</div>
<div class="msg-actions"></div>
%elif not is_id_verified or is_unverified:
<div class="msg-content">
## Translators: This message appears to users when the users have not completed identity verification.
<h2 class="title">${_("Certificate unavailable")}</h2>
<p class="copy">${_("You have not received a certificate because you do not have a current {platform_name} verified identity. ").format(platform_name=settings.PLATFORM_NAME)} <a href="${reverse('verify_student_reverify')}"> ${_("Verify your identity now.")}</a></p>
</div>
<div class="msg-actions"></div>
%else:
<div class="msg-content">
<h2 class="title">${_("Congratulations, you qualified for a certificate!")}</h2>
......
<%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:
<div class="message message-status ${status_css_class} is-shown">
% if cert_status['status'] == 'processing':
<p class="message-copy">${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}</p>
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'):
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing', 'unverified'):
<p class="message-copy">${_("Your final grade:")}
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% if cert_status['status'] == 'notpassing':
......@@ -48,6 +48,11 @@ else:
<p class="message-copy">
${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('<a class="contact-link" href="mailto:{email}">{email}</a>.').format(email=settings.CONTACT_EMAIL), cert_name_short=cert_name_short, cert_name_long=cert_name_long)}
</p>
% elif cert_status['status'] == 'unverified':
<p class="message-copy">
${Text(_("Your certificate was not issued because you do not have a current verified identity with {platform_name}. ")).format(platform_name=settings.PLATFORM_NAME)}
<a href="${reverify_link}"> ${Text(_("Verify your identity now."))}</a>
</p>
% endif
</p>
% endif
......
......@@ -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 (
<footer class="wrapper-messages-primary">
<ul class="messages-list">
% if course_overview.may_certify() and cert_status:
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course_overview=course_overview, enrollment=enrollment'/>
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course_overview=course_overview, enrollment=enrollment, reverify_link=reverify_link'/>
% endif
% if credit_status is not None:
......
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