Merge pull request #12484 from edx/schen/ECOM-4007
ECOM-4007 Add new cert status "unverified" to handle no active user ID verification
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
<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:
|
||||
|
||||
Reference in New Issue
Block a user