From 1ac94921c39b6062f3aec5b83930effc0b129b5b Mon Sep 17 00:00:00 2001
From: Tasawer <tasawer.nawaz@arbisoft.com>
Date: Tue, 27 Sep 2016 15:47:00 +0500
Subject: [PATCH] Show verification expiration message on learner dashboard and
 allow them to reverify if expiration is X days away. ECOM-2979

---
 common/djangoapps/student/helpers.py          | 31 +++++++++----
 .../student/tests/test_verification_status.py | 43 +++++++++++++++----
 lms/djangoapps/verify_student/models.py       | 38 +++++++++++++++-
 .../verify_student/tests/test_models.py       |  9 ++--
 .../verify_student/tests/test_views.py        | 17 ++++++++
 lms/djangoapps/verify_student/views.py        | 12 +++++-
 lms/envs/common.py                            |  2 +
 .../dashboard/_dashboard_course_listing.html  | 23 +++++-----
 .../_dashboard_status_verification.html       | 12 +++---
 9 files changed, 148 insertions(+), 39 deletions(-)

diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
index d5d8a915923..39b48ece318 100644
--- a/common/djangoapps/student/helpers.py
+++ b/common/djangoapps/student/helpers.py
@@ -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:
diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py
index 342ad05b073..a4e023f93a4 100644
--- a/common/djangoapps/student/tests/test_verification_status.py
+++ b/common/djangoapps/student/tests/test_verification_status.py
@@ -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):
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index 8f8d9631332..2a1f632fffc 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -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):
     """
diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py
index 96778e1b45d..4f726a90d18 100644
--- a/lms/djangoapps/verify_student/tests/test_models.py
+++ b/lms/djangoapps/verify_student/tests/test_models.py
@@ -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)
diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index 14a2f9d6c04..f2a1c7f18f2 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -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.
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index f849f4bee7f..9c37e8dc83b 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -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),
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 73c2d817951..b8cd837e9ba 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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 ###########
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index 651564f1c8d..14774c0e6a3 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -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
diff --git a/lms/templates/dashboard/_dashboard_status_verification.html b/lms/templates/dashboard/_dashboard_status_verification.html
index ac57f89543a..746ba5ef758 100644
--- a/lms/templates/dashboard/_dashboard_status_verification.html
+++ b/lms/templates/dashboard/_dashboard_status_verification.html
@@ -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>
-- 
GitLab