Show verification expiration message on learner dashboard and allow them to reverify if expiration is X days away.
ECOM-2979
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from datetime import datetime
|
||||
import urllib
|
||||
|
||||
from pytz import UTC
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from oauth2_provider.models import (
|
||||
AccessToken as dot_access_token,
|
||||
@@ -11,7 +12,6 @@ from provider.oauth2.models import (
|
||||
AccessToken as dop_access_token,
|
||||
RefreshToken as dop_refresh_token
|
||||
)
|
||||
from pytz import UTC
|
||||
|
||||
import third_party_auth
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification
|
||||
@@ -22,6 +22,7 @@ from course_modes.models import CourseMode
|
||||
# we display on the student dashboard.
|
||||
VERIFY_STATUS_NEED_TO_VERIFY = "verify_need_to_verify"
|
||||
VERIFY_STATUS_SUBMITTED = "verify_submitted"
|
||||
VERIFY_STATUS_RESUBMITTED = "re_verify_submitted"
|
||||
VERIFY_STATUS_APPROVED = "verify_approved"
|
||||
VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY = "verify_need_to_reverify"
|
||||
@@ -40,6 +41,8 @@ def check_verify_status_by_course(user, course_enrollments):
|
||||
* VERIFY_STATUS_NEED_TO_VERIFY: The student has not yet submitted photos for verification.
|
||||
* VERIFY_STATUS_SUBMITTED: The student has submitted photos for verification,
|
||||
but has have not yet been approved.
|
||||
* VERIFY_STATUS_RESUBMITTED: The student has re-submitted photos for re-verification while
|
||||
they still have an active but expiring ID verification
|
||||
* VERIFY_STATUS_APPROVED: The student has been successfully verified.
|
||||
* VERIFY_STATUS_MISSED_DEADLINE: The student did not submit photos within the course's deadline.
|
||||
* VERIFY_STATUS_NEED_TO_REVERIFY: The student has an active verification, but it is
|
||||
@@ -80,6 +83,11 @@ def check_verify_status_by_course(user, course_enrollments):
|
||||
user, queryset=verifications
|
||||
)
|
||||
|
||||
# Retrieve expiration_datetime of most recent approved verification
|
||||
# To avoid another database hit, we re-use the queryset we have already retrieved.
|
||||
expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(user, verifications)
|
||||
verification_expiring_soon = SoftwareSecurePhotoVerification.is_verification_expiring_soon(expiration_datetime)
|
||||
|
||||
# Retrieve verification deadlines for the enrolled courses
|
||||
enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
|
||||
course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys)
|
||||
@@ -112,9 +120,15 @@ def check_verify_status_by_course(user, course_enrollments):
|
||||
# Check whether the user was approved or is awaiting approval
|
||||
if relevant_verification is not None:
|
||||
if relevant_verification.status == "approved":
|
||||
status = VERIFY_STATUS_APPROVED
|
||||
if verification_expiring_soon:
|
||||
status = VERIFY_STATUS_NEED_TO_REVERIFY
|
||||
else:
|
||||
status = VERIFY_STATUS_APPROVED
|
||||
elif relevant_verification.status == "submitted":
|
||||
status = VERIFY_STATUS_SUBMITTED
|
||||
if verification_expiring_soon:
|
||||
status = VERIFY_STATUS_RESUBMITTED
|
||||
else:
|
||||
status = VERIFY_STATUS_SUBMITTED
|
||||
|
||||
# If the user didn't submit at all, then tell them they need to verify
|
||||
# If the deadline has already passed, then tell them they missed it.
|
||||
@@ -127,11 +141,12 @@ def check_verify_status_by_course(user, course_enrollments):
|
||||
)
|
||||
if status is None and not submitted:
|
||||
if deadline is None or deadline > datetime.now(UTC):
|
||||
if has_active_or_pending:
|
||||
# The user has an active verification, but the verification
|
||||
# is set to expire before the deadline. Tell the student
|
||||
# to reverify.
|
||||
status = VERIFY_STATUS_NEED_TO_REVERIFY
|
||||
if SoftwareSecurePhotoVerification.user_is_verified(user):
|
||||
if verification_expiring_soon:
|
||||
# The user has an active verification, but the verification
|
||||
# is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
|
||||
# Tell the student to reverify.
|
||||
status = VERIFY_STATUS_NEED_TO_REVERIFY
|
||||
else:
|
||||
status = VERIFY_STATUS_NEED_TO_VERIFY
|
||||
else:
|
||||
|
||||
@@ -8,10 +8,12 @@ from nose.plugins.attrib import attr
|
||||
from pytz import UTC
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
|
||||
from student.helpers import (
|
||||
VERIFY_STATUS_NEED_TO_VERIFY,
|
||||
VERIFY_STATUS_SUBMITTED,
|
||||
VERIFY_STATUS_RESUBMITTED,
|
||||
VERIFY_STATUS_APPROVED,
|
||||
VERIFY_STATUS_MISSED_DEADLINE,
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY
|
||||
@@ -192,6 +194,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
# messaging relating to verification
|
||||
self._assert_course_verification_status(None)
|
||||
|
||||
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
|
||||
def test_verification_will_expire_by_deadline(self):
|
||||
# Expiration date in the future
|
||||
self._setup_mode_and_enrollment(self.FUTURE, "verified")
|
||||
@@ -202,16 +205,36 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
|
||||
# This attempt will expire tomorrow, before the course deadline
|
||||
attempt.created_at = attempt.created_at - timedelta(days=364)
|
||||
attempt.approve()
|
||||
attempt.save()
|
||||
|
||||
# Expect that the "verify now" message is hidden
|
||||
# (since the user isn't allowed to submit another attempt while
|
||||
# a verification is active).
|
||||
# Verify that learner can submit photos if verification is set to expire soon.
|
||||
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)
|
||||
|
||||
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
|
||||
def test_reverification_submitted_with_current_approved_verificaiton(self):
|
||||
# Expiration date in the future
|
||||
self._setup_mode_and_enrollment(self.FUTURE, "verified")
|
||||
|
||||
# Create a verification attempt that is approved but expiring soon
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
attempt.approve()
|
||||
attempt.save()
|
||||
|
||||
# Verify that learner can submit photos if verification is set to expire soon.
|
||||
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_REVERIFY)
|
||||
|
||||
# Submit photos for reverification
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
|
||||
# Expect that learner has submitted photos for reverfication and his/her
|
||||
# previous verification is set to expired soon.
|
||||
self._assert_course_verification_status(VERIFY_STATUS_RESUBMITTED)
|
||||
|
||||
def test_verification_occurred_after_deadline(self):
|
||||
# Expiration date in the past
|
||||
self._setup_mode_and_enrollment(self.PAST, "verified")
|
||||
@@ -304,9 +327,10 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
"You still need to verify for this course.",
|
||||
"Verification not yet complete"
|
||||
],
|
||||
VERIFY_STATUS_SUBMITTED: ["Thanks for your patience as we process your request."],
|
||||
VERIFY_STATUS_APPROVED: ["You have already verified your ID!"],
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY: ["Your verification will expire soon!"]
|
||||
VERIFY_STATUS_SUBMITTED: ["You have submitted your verification information."],
|
||||
VERIFY_STATUS_RESUBMITTED: ["You have submitted your reverification information."],
|
||||
VERIFY_STATUS_APPROVED: ["You have successfully verified your ID with edX"],
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY: ["Your current verification will expire soon."]
|
||||
}
|
||||
|
||||
MODE_CLASSES = {
|
||||
@@ -315,7 +339,8 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
VERIFY_STATUS_SUBMITTED: "verified",
|
||||
VERIFY_STATUS_APPROVED: "verified",
|
||||
VERIFY_STATUS_MISSED_DEADLINE: "audit",
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY: "audit"
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY: "audit",
|
||||
VERIFY_STATUS_RESUBMITTED: "audit"
|
||||
}
|
||||
|
||||
def _assert_course_verification_status(self, status):
|
||||
|
||||
@@ -257,6 +257,28 @@ class PhotoVerification(StatusModel):
|
||||
)
|
||||
).order_by('-created_at')
|
||||
|
||||
@classmethod
|
||||
def get_expiration_datetime(cls, user, queryset=None):
|
||||
"""
|
||||
Check whether the user has an approved verification and return the
|
||||
"expiration_datetime" of most recent "approved" verification.
|
||||
|
||||
Arguments:
|
||||
user (Object): User
|
||||
queryset: If a queryset is provided, that will be used instead
|
||||
of hitting the database.
|
||||
|
||||
Returns:
|
||||
expiration_datetime: expiration_datetime of most recent "approved"
|
||||
verification.
|
||||
"""
|
||||
if queryset is None:
|
||||
queryset = cls.objects.filter(user=user)
|
||||
|
||||
photo_verification = queryset.filter(status='approved').first()
|
||||
if photo_verification:
|
||||
return photo_verification.expiration_datetime
|
||||
|
||||
@classmethod
|
||||
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
|
||||
"""
|
||||
@@ -384,7 +406,7 @@ class PhotoVerification(StatusModel):
|
||||
|
||||
Arguments:
|
||||
deadline (datetime): The date at which the verification was active
|
||||
(created before and expired after).
|
||||
(created before and expiration datetime is after today).
|
||||
|
||||
Returns:
|
||||
bool
|
||||
@@ -392,7 +414,7 @@ class PhotoVerification(StatusModel):
|
||||
"""
|
||||
return (
|
||||
self.created_at < deadline and
|
||||
self.expiration_datetime > deadline
|
||||
self.expiration_datetime > datetime.now(pytz.UTC)
|
||||
)
|
||||
|
||||
def parsed_error_msg(self):
|
||||
@@ -944,6 +966,18 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
else:
|
||||
return 'ID Verified'
|
||||
|
||||
@classmethod
|
||||
def is_verification_expiring_soon(cls, expiration_datetime):
|
||||
"""
|
||||
Returns True if verification is expiring within EXPIRING_SOON_WINDOW.
|
||||
"""
|
||||
if expiration_datetime:
|
||||
if (expiration_datetime - datetime.now(pytz.UTC)).days <= settings.VERIFY_STUDENT.get(
|
||||
"EXPIRING_SOON_WINDOW"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class VerificationDeadline(TimeStampedModel):
|
||||
"""
|
||||
|
||||
@@ -382,8 +382,9 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
|
||||
self.assertTrue(attempt.active_at_datetime(before_expiration))
|
||||
|
||||
# Not active after the expiration date
|
||||
after = expiration + timedelta(seconds=1)
|
||||
self.assertFalse(attempt.active_at_datetime(after))
|
||||
attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
||||
attempt.save()
|
||||
self.assertFalse(attempt.active_at_datetime(datetime.now(pytz.UTC) + timedelta(days=1)))
|
||||
|
||||
def test_verification_for_datetime(self):
|
||||
user = UserFactory.create()
|
||||
@@ -427,7 +428,9 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
|
||||
self.assertEqual(result, attempt)
|
||||
|
||||
# Immediately after the expiration date, should not get the attempt
|
||||
after = expiration + timedelta(seconds=1)
|
||||
attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
||||
attempt.save()
|
||||
after = datetime.now(pytz.UTC) + timedelta(days=1)
|
||||
query = SoftwareSecurePhotoVerification.objects.filter(user=user)
|
||||
result = SoftwareSecurePhotoVerification.verification_for_datetime(after, query)
|
||||
self.assertIs(result, None)
|
||||
|
||||
@@ -2071,6 +2071,23 @@ class TestReverifyView(TestCase):
|
||||
# Cannot reverify because the user is already verified.
|
||||
self._assert_cannot_reverify()
|
||||
|
||||
@override_settings(VERIFY_STUDENT={"DAYS_GOOD_FOR": 5, "EXPIRING_SOON_WINDOW": 10})
|
||||
def test_reverify_view_can_reverify_approved_expired_soon(self):
|
||||
"""
|
||||
Verify that learner can submit photos if verification is set to expired soon.
|
||||
Verification will be good for next DAYS_GOOD_FOR (i.e here it is 5 days) days,
|
||||
and learner can submit photos if verification is set to expire in
|
||||
EXPIRING_SOON_WINDOW(i.e here it is 10 days) or less days.
|
||||
"""
|
||||
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.submit()
|
||||
attempt.approve()
|
||||
|
||||
# Can re-verify because verification is set to expired soon.
|
||||
self._assert_can_reverify()
|
||||
|
||||
def _get_reverify_page(self):
|
||||
"""
|
||||
Retrieve the reverification page and return the response.
|
||||
|
||||
@@ -1376,12 +1376,22 @@ class ReverifyView(View):
|
||||
"""
|
||||
status, _ = SoftwareSecurePhotoVerification.user_status(request.user)
|
||||
|
||||
expiration_datetime = SoftwareSecurePhotoVerification.get_expiration_datetime(request.user)
|
||||
can_reverify = False
|
||||
if expiration_datetime:
|
||||
if SoftwareSecurePhotoVerification.is_verification_expiring_soon(expiration_datetime):
|
||||
# The user has an active verification, but the verification
|
||||
# is set to expire within "EXPIRING_SOON_WINDOW" days (default is 4 weeks).
|
||||
# In this case user can resubmit photos for reverification.
|
||||
can_reverify = True
|
||||
|
||||
# If the user has no initial verification or if the verification
|
||||
# process is still ongoing 'pending' or expired then allow the user to
|
||||
# submit the photo verification.
|
||||
# A photo verification is marked as 'pending' if its status is either
|
||||
# 'submitted' or 'must_retry'.
|
||||
if status in ["none", "must_reverify", "expired", "pending"]:
|
||||
|
||||
if status in ["none", "must_reverify", "expired", "pending"] or can_reverify:
|
||||
context = {
|
||||
"user_full_name": request.user.profile.name,
|
||||
"platform_name": configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
|
||||
|
||||
@@ -2288,6 +2288,8 @@ MOBILE_STORE_URLS = {
|
||||
################# Student Verification #################
|
||||
VERIFY_STUDENT = {
|
||||
"DAYS_GOOD_FOR": 365, # How many days is a verficiation good for?
|
||||
# The variable represents the window within which a verification is considered to be "expiring soon."
|
||||
"EXPIRING_SOON_WINDOW": 28,
|
||||
}
|
||||
|
||||
### This enables the Metrics tab for the Instructor dashboard ###########
|
||||
|
||||
@@ -14,6 +14,7 @@ from openedx.core.lib.time_zone_utils import get_user_time_zone
|
||||
from student.helpers import (
|
||||
VERIFY_STATUS_NEED_TO_VERIFY,
|
||||
VERIFY_STATUS_SUBMITTED,
|
||||
VERIFY_STATUS_RESUBMITTED,
|
||||
VERIFY_STATUS_APPROVED,
|
||||
VERIFY_STATUS_MISSED_DEADLINE,
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY,
|
||||
@@ -299,7 +300,7 @@ from student.helpers import (
|
||||
<%include file="_dashboard_credit_info.html" args="credit_status=credit_status"/>
|
||||
% endif
|
||||
|
||||
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY] and not is_course_blocked:
|
||||
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_RESUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY] and not is_course_blocked:
|
||||
<div class="message message-status wrapper-message-primary is-shown">
|
||||
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
|
||||
<div class="verification-reminder">
|
||||
@@ -319,22 +320,24 @@ from student.helpers import (
|
||||
<a href="${reverse('verify_student_verify_now', kwargs={'course_id': unicode(course_overview.id)})}" class="btn" data-course-id="${course_overview.id}">${_('Verify Now')}</a>
|
||||
</div>
|
||||
% elif verification_status['status'] == VERIFY_STATUS_SUBMITTED:
|
||||
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
|
||||
<p class="message-copy">${_('Thanks for your patience as we process your request.')}</p>
|
||||
<h4 class="message-title">${_('You have submitted your verification information.')}</h4>
|
||||
<p class="message-copy">${_('You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).')}</p>
|
||||
% elif verification_status['status'] == VERIFY_STATUS_RESUBMITTED:
|
||||
<h4 class="message-title">${_('Your current verification will expire soon!')}</h4>
|
||||
<p class="message-copy">${_('You have submitted your reverification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).')}</p>
|
||||
% elif verification_status['status'] == VERIFY_STATUS_APPROVED:
|
||||
<h4 class="message-title">${_('You have already verified your ID!')}</h4>
|
||||
<h4 class="message-title">${_('You have successfully verified your ID with edX')}</h4>
|
||||
% if verification_status.get('verification_good_until') is not None:
|
||||
<p class="message-copy">${_('Your verification status is good until {date}.').format(date=verification_status['verification_good_until'])}
|
||||
<p class="message-copy">${_('Your current verification is effective until {date}.').format(date=verification_status['verification_good_until'])}
|
||||
% endif
|
||||
% elif verification_status['status'] == VERIFY_STATUS_NEED_TO_REVERIFY:
|
||||
<h4 class="message-title">${_('Your verification will expire soon!')}</h4>
|
||||
<h4 class="message-title">${_('Your current verification will expire soon.')}</h4>
|
||||
## Translators: start_link and end_link will be replaced with HTML tags;
|
||||
## please do not translate these.
|
||||
<p class="message-copy">${Text(_('Your current verification will expire before the verification deadline '
|
||||
'for this course. {start_link}Re-verify your identity now{end_link} using a webcam and a '
|
||||
'government-issued ID.')).format(
|
||||
<p class="message-copy">${Text(_('Your current verification will expire in {days} days. {start_link}Re-verify your identity now{end_link} using a webcam and a government-issued photo ID.')).format(
|
||||
start_link=HTML('<a href="{href}">').format(href=reverse('verify_student_reverify')),
|
||||
end_link=HTML('</a>')
|
||||
end_link=HTML('</a>'),
|
||||
days=settings.VERIFY_STUDENT.get("EXPIRING_SOON_WINDOW")
|
||||
)}
|
||||
</p>
|
||||
% endif
|
||||
|
||||
@@ -7,22 +7,22 @@ from django.core.urlresolvers import reverse
|
||||
|
||||
%if verification_status == 'approved':
|
||||
<li class="status status-verification is-accepted">
|
||||
<span class="title status-title">${_("Verification Status: Approved")}</span>
|
||||
<p class="status-note">${_("Your edX Verification is reviewed and approved. Your verification status is good for one year after submission.")}</p>
|
||||
<span class="title status-title">${_("Current Verification Status: Approved")}</span>
|
||||
<p class="status-note">${_("Your edX verification has been approved. Your verification is effective for one year after submission.")}</p>
|
||||
</li>
|
||||
%endif
|
||||
|
||||
%if verification_status == 'pending':
|
||||
<li class="status status-verification is-pending">
|
||||
<span class="title status-title">${_("Verification Status: Pending")}</span>
|
||||
<p class="status-note">${_("Your edX Verification is pending. Your verification photos have been submitted and will be reviewed shortly.")}</p>
|
||||
<span class="title status-title">${_("Current Verification Status: Pending")}</span>
|
||||
<p class="status-note">${_("Your edX ID verification is pending. Your verification information has been submitted and will be reviewed shortly.")}</p>
|
||||
</li>
|
||||
%endif
|
||||
|
||||
%if verification_status in ['must_reverify', 'expired']:
|
||||
<li class="status status-verification is-denied">
|
||||
<span class="title status-title">${_("Verification Status: Expired")}</span>
|
||||
<p class="status-note">${_("Your edX Verification has expired. To receive a verified certificate, you have to submit a new photo of yourself and your government-issued photo ID before the course ends.")}</p>
|
||||
<span class="title status-title">${_("Current Verification Status: Expired")}</span>
|
||||
<p class="status-note">${_("Your verification has expired. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}</p>
|
||||
<div class="btn-reverify">
|
||||
<a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user