Commit b4dd18b1 by Simon Chen Committed by GitHub

Merge pull request #13578 from edx/tasawer/ecom-2979/learner-reverification-flow-update

Fixing issues with photo verification flow
parents b5e0d547 1ac94921
......@@ -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