Commit 1ac94921 by Tasawer Committed by tasawernawaz

Show verification expiration message on learner dashboard and allow them to…

Show verification expiration message on learner dashboard and allow them to reverify if expiration is X days away.
ECOM-2979
parent 6e23a2ca
......@@ -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()
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)
# This attempt will expire tomorrow, before the course deadline
attempt.created_at = attempt.created_at - timedelta(days=364)
@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()
# 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)
# 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):
......
......@@ -258,6 +258,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):
"""
Check whether the user has an active or pending verification attempt
......@@ -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>
......
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