From bf210398e6970c42f5573627e14a5162def25e2c Mon Sep 17 00:00:00 2001
From: Julia Hansbrough <julia@edx.org>
Date: Wed, 22 Jan 2014 22:16:01 +0000
Subject: [PATCH] Tests and cleanup

Improved tests, fixed javascript for photocapture, removed extraneous photocapture code, added time & course numbers to templates for design
---
 common/djangoapps/student/views.py            |  93 ++---
 lms/djangoapps/verify_student/exceptions.py   |   9 +
 ...onwindow__add_field_softwaresecurephoto.py |   4 +-
 lms/djangoapps/verify_student/models.py       |  38 ++-
 .../verify_student/tests/test_models.py       |   4 +-
 .../verify_student/tests/test_views.py        |  33 +-
 lms/djangoapps/verify_student/urls.py         |   6 +
 lms/djangoapps/verify_student/views.py        |  28 +-
 lms/static/js/verify_student/photocapture.js  |  20 +-
 .../js/verify_student/photocapturebasic2.js   | 322 ------------------
 .../_dashboard_prompt_midcourse_reverify.html |   2 +-
 .../midcourse_photo_reverification.html       |   4 +-
 .../midcourse_reverify_dash.html              |   2 +-
 .../reverification_window_expired.html        |  45 +++
 14 files changed, 201 insertions(+), 409 deletions(-)
 create mode 100644 lms/djangoapps/verify_student/exceptions.py
 delete mode 100644 lms/static/js/verify_student/photocapturebasic2.js
 create mode 100644 lms/templates/verify_student/reverification_window_expired.html

diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 585da3d7136..bbbad37f04c 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -182,6 +182,52 @@ def cert_info(user, course):
     return _cert_info(user, course, certificate_status_for_student(user, course.id))
 
 
+def reverification_info(user, course, enrollment):
+    """
+    If "user" currently needs to be reverified in "course", this returns a four-tuple with re-verification
+    info for views to display.  Else, returns None.
+
+    Four-tuple data: (course_id, course_display_name, reverification_end_date, reverification_status)
+    """
+    # IF the reverification window is open
+    if (MidcourseReverificationWindow.window_open_for_course(course.id)):
+        # AND the user is actually verified-enrolled AND they don't have a pending reverification already
+        window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
+        if (enrollment.mode == "verified" and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(user, window=window)):
+            window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
+            return (
+                course.id,
+                course.display_name,
+                course.number,
+                window.end_date.strftime('%B %d, %Y %X %p'),
+                "must_reverify"  # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure)
+            )
+    return None
+
+
+def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
+    course_enrollment_pairs = []
+    for enrollment in CourseEnrollment.enrollments_for_user(user):
+        try:
+            course = course_from_id(enrollment.course_id)
+
+            # if we are in a Microsite, then filter out anything that is not
+            # attributed (by ORG) to that Microsite
+            if course_org_filter and course_org_filter != course.location.org:
+                continue
+            # Conversely, if we are not in a Microsite, then let's filter out any enrollments
+            # with courses attributed (by ORG) to Microsites
+            elif course.location.org in org_filter_out_set:
+                continue
+
+            course_enrollment_pairs.append((course, enrollment))
+        except ItemNotFoundError:
+            log.error("User {0} enrolled in non-existent course {1}"
+                      .format(user.username, enrollment.course_id))
+    return course_enrollment_pairs
+
+
+
 def _cert_info(user, course, cert_status):
     """
     Implements the logic for cert_info -- split out for testing.
@@ -323,11 +369,6 @@ def complete_course_mode_info(course_id, enrollment):
 def dashboard(request):
     user = request.user
 
-    # Build our (course, enrollment) list for the user, but ignore any courses that no
-    # longer exist (because the course IDs have changed). Still, we don't delete those
-    # enrollments, because it could have been a data push snafu.
-    course_enrollment_pairs = []
-
     # for microsites, we want to filter and only show enrollments for courses within
     # the microsites 'ORG'
     course_org_filter = MicrositeConfiguration.get_microsite_configuration_value('course_org_filter')
@@ -340,23 +381,10 @@ def dashboard(request):
     if course_org_filter:
         org_filter_out_set.remove(course_org_filter)
 
-    for enrollment in CourseEnrollment.enrollments_for_user(user):
-        try:
-            course = course_from_id(enrollment.course_id)
-
-            # if we are in a Microsite, then filter out anything that is not
-            # attributed (by ORG) to that Microsite
-            if course_org_filter and course_org_filter != course.location.org:
-                continue
-            # Conversely, if we are not in a Microsite, then let's filter out any enrollments
-            # with courses attributed (by ORG) to Microsites
-            elif course.location.org in org_filter_out_set:
-                continue
-
-            course_enrollment_pairs.append((course, enrollment))
-        except ItemNotFoundError:
-            log.error(u"User {0} enrolled in non-existent course {1}"
-                      .format(user.username, enrollment.course_id))
+    # Build our (course, enrollment) list for the user, but ignore any courses that no
+    # longer exist (because the course IDs have changed). Still, we don't delete those
+    # enrollments, because it could have been a data push snafu.
+    course_enrollment_pairs = get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set)
 
     course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
 
@@ -392,25 +420,12 @@ def dashboard(request):
     # TODO: make this banner appear at the top of courseware as well
     verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
 
-    # TODO: Factor this out into a function; I'm pretty sure there's code duplication floating around...
+    # Gets data for midcourse reverifications, if any are necessary or have failed
     reverify_course_data = []
     for (course, enrollment) in course_enrollment_pairs:
-
-        # IF the reverification window is open
-        if (MidcourseReverificationWindow.window_open_for_course(course.id)):
-            # AND the user is actually verified-enrolled AND they don't have a pending reverification already
-            window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
-            if (enrollment.mode == "verified" and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(user, window=window)):
-                window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
-                status_for_window = SoftwareSecurePhotoVerification.user_status(user, window=window)
-                reverify_course_data.append(
-                    (
-                        course.id,
-                        course.display_name,
-                        window.end_date,
-                        "must_reverify"  # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure)
-                    )
-                )
+        info = reverification_info(user, course, enrollment)
+        if info:
+            reverify_course_data.append(info)
 
     show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
                                        if _enrollment.refundable())
diff --git a/lms/djangoapps/verify_student/exceptions.py b/lms/djangoapps/verify_student/exceptions.py
new file mode 100644
index 00000000000..d31fdb6a6d0
--- /dev/null
+++ b/lms/djangoapps/verify_student/exceptions.py
@@ -0,0 +1,9 @@
+"""
+Exceptions for the verify student app
+"""
+# (Exception Class Names are sort of self-explanatory, so skipping docstring requirement)
+# pylint: disable=C0111
+
+
+class WindowExpiredException(Exception):
+    pass
diff --git a/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow__add_field_softwaresecurephoto.py b/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow__add_field_softwaresecurephoto.py
index 92ec4fdb4c2..537cd36c3d0 100644
--- a/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow__add_field_softwaresecurephoto.py
+++ b/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow__add_field_softwaresecurephoto.py
@@ -22,7 +22,6 @@ class Migration(SchemaMigration):
                       self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.MidcourseReverificationWindow'], null=True),
                       keep_default=False)
 
-
     def backwards(self, orm):
         # Deleting model 'MidcourseReverificationWindow'
         db.delete_table('verify_student_midcoursereverificationwindow')
@@ -30,7 +29,6 @@ class Migration(SchemaMigration):
         # Deleting field 'SoftwareSecurePhotoVerification.window'
         db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
 
-
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -97,4 +95,4 @@ class Migration(SchemaMigration):
         }
     }
 
-    complete_apps = ['verify_student']
\ No newline at end of file
+    complete_apps = ['verify_student']
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index b406cfa25b8..41d4cb650df 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -23,7 +23,7 @@ import pytz
 import requests
 
 from django.conf import settings
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.contrib.auth.models import User
@@ -71,17 +71,9 @@ class MidcourseReverificationWindow(models.Model):
         Returns a boolean, True if the course is currently asking for reverification, else False.
         """
         now = datetime.now(pytz.UTC)
-
-        try:
-            cls.objects.get(
-                course_id=course_id,
-                start_date__lte=now,
-                end_date__gte=now,
-            )
-        except(ObjectDoesNotExist):
-            return False
-
-        return True
+        if cls.get_window(course_id, now):
+            return True
+        return False
 
     @classmethod
     def get_window(cls, course_id, date):
@@ -244,9 +236,13 @@ class PhotoVerification(StatusModel):
     @classmethod
     def user_is_verified(cls, user, earliest_allowed_date=None, window=None):
         """
-        Return whether or not a user has satisfactorily proved their
-        identity wrt to the INITIAL verification. Depending on the policy,
-        this can expire after some period of time, so a user might have to renew periodically.
+        Return whether or not a user has satisfactorily proved their identity.
+        Depending on the policy, this can expire after some period of time, so
+        a user might have to renew periodically.
+
+        If window=None, then this will check for the user's *initial* verification.
+        If window is set to anything else, it will check for the reverification
+        associated with that window.
         """
         return cls.objects.filter(
             user=user,
@@ -264,6 +260,10 @@ class PhotoVerification(StatusModel):
         have been submitted but had an non-user error when it was being
         submitted. It's basically any situation in which the user has signed off
         on the contents of the attempt, and we have not yet received a denial.
+
+        If window=None, this will check for the user's *initial* verification.  If
+        window is anything else, this will check for the reverification associated
+        with that window.
         """
         if window:
             valid_statuses = ['submitted', 'approved']
@@ -280,8 +280,11 @@ class PhotoVerification(StatusModel):
     @classmethod
     def active_for_user(cls, user, window=None):
         """
-        Return the most recent INITIAL PhotoVerification that is marked ready (i.e. the
+        Return the most recent PhotoVerification that is marked ready (i.e. the
         user has said they're set, but we haven't submitted anything yet).
+
+        If window=None, this checks for the original verification.  If window is set to
+        anything else, this will check for the reverification associated with that window.
         """
         # This should only be one at the most, but just in case we create more
         # by mistake, we'll grab the most recently created one.
@@ -301,6 +304,9 @@ class PhotoVerification(StatusModel):
         If the verification has been approved, returns 'approved'
         If the verification process is still ongoing, returns 'pending'
         If the verification has been denied and the user must resubmit photos, returns 'must_reverify'
+
+        If window=None, this checks initial verifications
+        If window is set, this checks for the reverification associated with that window
         """
         status = 'none'
         error_msg = ''
diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py
index 26c4149dc3b..e2fb298ed94 100644
--- a/lms/djangoapps/verify_student/tests/test_models.py
+++ b/lms/djangoapps/verify_student/tests/test_models.py
@@ -438,6 +438,7 @@ class TestMidcourseReverificationWindow(TestCase):
 @patch('verify_student.models.Key', new=MockKey)
 @patch('verify_student.models.requests.post', new=mock_software_secure_post)
 class TestMidcourseReverification(TestCase):
+    """ Tests for methods that are specific to midcourse SoftwareSecurePhotoVerification objects """
     def setUp(self):
         self.course_id = "MITx/999/Robot_Super_Course"
         self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
@@ -527,6 +528,3 @@ class TestMidcourseReverification(TestCase):
         attempt.status = "approved"
         attempt.save()
         assert_true(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user=self.user, window=window))
-
-    def test_active_for_user(self):
-        pass
diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index a6dd07b4892..5ab39dda9bb 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -11,6 +11,8 @@ verify_student/start?course_id=MITx/6.002x/2013_Spring # create
 """
 import urllib
 from mock import patch, Mock, ANY
+import pytz
+from datetime import timedelta, datetime
 
 from django.test import TestCase
 from django.test.utils import override_settings
@@ -114,24 +116,14 @@ class TestReverifyView(TestCase):
             self.assertIsNotNone(verification_attempt)
         except ObjectDoesNotExist:
             self.fail('No verification object generated')
+        ((template, context), _kwargs) = render_mock.call_args
         self.assertIn('photo_reverification', template)
         self.assertTrue(context['error'])
 
-    @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
-    def test_reverify_post_success(self):
-        url = reverse('verify_student_reverify')
-        response = self.client.post(url, {'face_image': ',',
-                                          'photo_id_image': ','})
-        self.assertEquals(response.status_code, 302)
-        try:
-            verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
-            self.assertIsNotNone(verification_attempt)
-        except ObjectDoesNotExist:
-            self.fail('No verification object generated')
-
 
 @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
 class TestMidCourseReverifyView(TestCase):
+    """ Tests for the midcourse reverification views """
     def setUp(self):
         self.user = UserFactory.create(username="rusty", password="test")
         self.client.login(username="rusty", password="test")
@@ -149,16 +141,29 @@ class TestMidCourseReverifyView(TestCase):
 
     @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
     def test_midcourse_reverify_post_success(self):
+        window = MidcourseReverificationWindowFactory(course_id=self.course_id)
         url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
         response = self.client.post(url, {'face_image': ','})
         self.assertEquals(response.status_code, 302)
         try:
-            verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
+            verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
             self.assertIsNotNone(verification_attempt)
         except ObjectDoesNotExist:
             self.fail('No verification object generated')
 
-    # TODO make this test more detailed
+    @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
+    def test_midcourse_reverify_post_failure_expired_window(self):
+        window = MidcourseReverificationWindowFactory(
+            course_id=self.course_id,
+            start_date=datetime.now(pytz.UTC) - timedelta(days=100),
+            end_date=datetime.now(pytz.UTC) - timedelta(days=50),
+        )
+        url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
+        response = self.client.post(url, {'face_image': ','})
+        self.assertEquals(response.status_code, 302)
+        with self.assertRaises(ObjectDoesNotExist):
+            SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
+
     @patch('verify_student.views.render_to_response', render_mock)
     def test_midcourse_reverify_dash(self):
         url = reverse('verify_student_midcourse_reverify_dash')
diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py
index 40b434ecea1..fe9b233cb11 100644
--- a/lms/djangoapps/verify_student/urls.py
+++ b/lms/djangoapps/verify_student/urls.py
@@ -64,4 +64,10 @@ urlpatterns = patterns(
         views.midcourse_reverify_dash,
         name="verify_student_midcourse_reverify_dash"
     ),
+
+    url(
+        r'^reverification_window_expired$',
+        views.reverification_window_expired,
+        name="verify_student_reverification_window_expired"
+    ),
 )
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 283d03fa64f..15c5e04d7ec 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -34,6 +34,7 @@ from verify_student.models import (
 )
 import ssencrypt
 from xmodule.modulestore.exceptions import ItemNotFoundError
+from .exceptions import WindowExpiredException
 
 log = logging.getLogger(__name__)
 
@@ -362,9 +363,11 @@ class MidCourseReverifyView(View):
         submits the reverification to SoftwareSecure
         """
         try:
-            # TODO look at this more carefully! #1 testing candidate
             now = datetime.datetime.now(UTC)
-            attempt = SoftwareSecurePhotoVerification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now))
+            window = MidcourseReverificationWindow.get_window(course_id, now)
+            if window is None:
+                raise WindowExpiredException
+            attempt = SoftwareSecurePhotoVerification(user=request.user, window=window)
             b64_face_image = request.POST['face_image'].split(",")[1]
 
             attempt.upload_face_image(b64_face_image.decode('base64'))
@@ -374,6 +377,13 @@ class MidCourseReverifyView(View):
             attempt.save()
             attempt.submit()
             return HttpResponseRedirect(reverse('verify_student_midcourse_reverification_confirmation'))
+
+        except WindowExpiredException:
+            log.exception(
+                "User {} attempted to re-verify, but the window expired before the attempt".format(request.user.id)
+            )
+            return HttpResponseRedirect(reverse('verify_student_reverification_window_expired'))
+
         except Exception:
             log.exception(
                 "Could not submit verification attempt for user {}".format(request.user.id)
@@ -390,7 +400,6 @@ def midcourse_reverify_dash(_request):
     Shows the "course reverification dashboard", which displays the reverification status (must reverify,
     pending, approved, failed, etc) of all courses in which a student has a verified enrollment.
     """
-    # TODO same comment as in student/views.py: need to factor out this functionality
     user = _request.user
     course_enrollment_pairs = []
     for enrollment in CourseEnrollment.enrollments_for_user(user):
@@ -406,7 +415,8 @@ def midcourse_reverify_dash(_request):
                 (
                     course.id,
                     course.display_name,
-                    MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC)).end_date,
+                    course.number,
+                    MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC)).end_date.strftime('%B %d, %Y %X %p'),
                     "must_reverify"
                 )
             )
@@ -431,3 +441,13 @@ def midcourse_reverification_confirmation(_request):
     Shows the user a confirmation page if the submission to SoftwareSecure was successful
     """
     return render_to_response("verify_student/midcourse_reverification_confirmation.html")
+
+
+@login_required
+def reverification_window_expired(_request):
+    """
+    Displays an error page if a student tries to submit a reverification, but the window
+    for that reverification has already expired.
+    """
+    # TODO need someone to review the copy for this template
+    return render_to_response("verify_student/reverification_window_expired.html")
diff --git a/lms/static/js/verify_student/photocapture.js b/lms/static/js/verify_student/photocapture.js
index 4db15e9d51f..f8c89ae0db7 100644
--- a/lms/static/js/verify_student/photocapture.js
+++ b/lms/static/js/verify_student/photocapture.js
@@ -35,15 +35,22 @@ var submitReverificationPhotos = function() {
 
 }
 
+var submitMidcourseReverificationPhotos = function() {
+  $('<input>').attr({
+      type: 'hidden',
+      name: 'face_image',
+      value: $("#face_image")[0].src,
+  }).appendTo("#reverify_form");
+  $("#reverify_form").submit();
+}
+
 var submitToPaymentProcessing = function() {
   var contribution_input = $("input[name='contribution']:checked")
   var contribution = 0;
-  if(contribution_input.attr('id') == 'contribution-other')
-  {
+  if(contribution_input.attr('id') == 'contribution-other') {
       contribution = $("input[name='contribution-other-amt']").val();
   }
-  else
-  {
+  else {
       contribution = contribution_input.val();
   }
   var course_id = $("input[name='course_id']").val();
@@ -276,11 +283,16 @@ $(document).ready(function() {
       submitReverificationPhotos();
   });
 
+  $("#midcourse_reverify_button").click(function() {
+      submitMidcourseReverificationPhotos();
+  });
+
   // prevent browsers from keeping this button checked
   $("#confirm_pics_good").prop("checked", false)
   $("#confirm_pics_good").change(function() {
       $("#pay_button").toggleClass('disabled');
       $("#reverify_button").toggleClass('disabled');
+      $("#midcourse_reverify_button").toggleClass('disabled');
   });
 
 
diff --git a/lms/static/js/verify_student/photocapturebasic2.js b/lms/static/js/verify_student/photocapturebasic2.js
deleted file mode 100644
index 7bf34582cd1..00000000000
--- a/lms/static/js/verify_student/photocapturebasic2.js
+++ /dev/null
@@ -1,322 +0,0 @@
-var onVideoFail = function(e) {
-  if(e == 'NO_DEVICES_FOUND') {
-      $('#no-webcam').show();
-      $('#face_capture_button').hide();
-  }
-  else {
-    console.log('Failed to get camera access!', e);
-  }
-};
-
-// Returns true if we are capable of video capture (regardless of whether the
-// user has given permission).
-function initVideoCapture() {
-  window.URL = window.URL || window.webkitURL;
-  navigator.getUserMedia  = navigator.getUserMedia || navigator.webkitGetUserMedia ||
-                            navigator.mozGetUserMedia || navigator.msGetUserMedia;
-  return !(navigator.getUserMedia == undefined);
-}
-
-var submitReverificationPhotos = function() {
-    // add photos to the form
-    $('<input>').attr({
-        type: 'hidden',
-        name: 'face_image',
-        value: $("#face_image")[0].src,
-    }).appendTo("#reverify_form");
-    // there is a change here
-    $("#reverify_form").submit();
-
-}
-
-var submitToPaymentProcessing = function() {
-  var contribution_input = $("input[name='contribution']:checked")
-  var contribution = 0;
-  if(contribution_input.attr('id') == 'contribution-other')
-  {
-      contribution = $("input[name='contribution-other-amt']").val();
-  }
-  else
-  {
-      contribution = contribution_input.val();
-  }
-  var course_id = $("input[name='course_id']").val();
-  var xhr = $.post(
-    "/verify_student/create_order",
-    {
-      "course_id" : course_id,
-      "contribution": contribution,
-      "face_image" : $("#face_image")[0].src,
-      // there is a change here
-    },
-    function(data) {
-      for (prop in data) {
-        $('<input>').attr({
-            type: 'hidden',
-            name: prop,
-            value: data[prop]
-        }).appendTo('#pay_form');
-      }
-    }
-  )
-  .done(function(data) {
-    $("#pay_form").submit();
-  })
-  .fail(function(jqXhr,text_status, error_thrown) {
-      if(jqXhr.status == 400) {
-          $('#order-error .copy p').html(jqXhr.responseText);
-      }
-      $('#order-error').show();
-      $("html, body").animate({ scrollTop: 0 });
-  });
-}
-
-function doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink) {
-  approveButton.removeClass('approved');
-  nextButtonNav.addClass('is-not-ready');
-  nextLink.attr('href', "#");
-
-  captureButton.show();
-  resetButton.hide();
-  approveButton.hide();
-}
-
-function doApproveButton(approveButton, nextButtonNav, nextLink) {
-  nextButtonNav.removeClass('is-not-ready');
-  approveButton.addClass('approved');
-  nextLink.attr('href', "#next");
-}
-
-function doSnapshotButton(captureButton, resetButton, approveButton) {
-  captureButton.hide();
-  resetButton.show();
-  approveButton.show();
-}
-
-function submitNameChange(event) {
-  event.preventDefault();
-  $("#lean_overlay").fadeOut(200);
-  $("#edit-name").css({ 'display' : 'none' });
-  var full_name = $('input[name="name"]').val();
-  var xhr = $.post(
-    "/change_name",
-    {
-      "new_name" : full_name,
-      "rationale": "Want to match ID for ID Verified Certificates."
-    },
-    function(data) {
-        $('#full-name').html(full_name);
-    }
-  )
-  .fail(function(jqXhr,text_status, error_thrown) {
-    $('.message-copy').html(jqXhr.responseText);
-  });
-
-}
-
-function initSnapshotHandler(names, hasHtml5CameraSupport) {
-  var name = names.pop();
-  if (name == undefined) {
-    return;
-  }
-
-  var video = $('#' + name + '_video');
-  var canvas = $('#' + name + '_canvas');
-  var image = $('#' + name + "_image");
-  var captureButton = $("#" + name + "_capture_button");
-  var resetButton = $("#" + name + "_reset_button");
-  var approveButton = $("#" + name + "_approve_button");
-  var nextButtonNav = $("#" + name + "_next_button_nav");
-  var nextLink = $("#" + name + "_next_link");
-  var flashCapture = $("#" + name + "_flash");
-
-  var ctx = null;
-  if (hasHtml5CameraSupport) {
-    ctx = canvas[0].getContext('2d');
-  }
-
-  var localMediaStream = null;
-
-  function snapshot(event) {
-    if (hasHtml5CameraSupport) {
-      if (localMediaStream) {
-        ctx.drawImage(video[0], 0, 0);
-        // TODO put this back eventually
-        image[0] = image[0];
-        image[0].src = image[0].src;
-        image[0].src = canvas[0].toDataURL('image/png');
-      }
-      else {
-        return false;
-      }
-      video[0].pause();
-    }
-    else {
-      if (flashCapture[0].cameraAuthorized()) {
-        image[0].src = flashCapture[0].snap();
-      }
-      else {
-        return false;
-      }
-    }
-
-    doSnapshotButton(captureButton, resetButton, approveButton);
-    return false;
-  }
-
-  
-  function reset() {
-    image[0].src = "";
-
-    if (hasHtml5CameraSupport) {
-      video[0].play();
-    }
-    else {
-      flashCapture[0].reset();
-    }
-
-    doResetButton(resetButton, captureButton, approveButton, nextButtonNav, nextLink);
-    return false;
-  }
-
-  function approve() {
-    doApproveButton(approveButton, nextButtonNav, nextLink)
-    return false;
-  }
-
-  // Initialize state for this picture taker
-  captureButton.show();
-  resetButton.hide();
-  approveButton.hide();
-  nextButtonNav.addClass('is-not-ready');
-  nextLink.attr('href', "#");
-
-  // Connect event handlers...
-  video.click(snapshot);
-  captureButton.click(snapshot);
-  resetButton.click(reset);
-  approveButton.click(approve);
-
-  // If it's flash-based, we can just immediate initialize the next one.
-  // If it's HTML5 based, we have to do it in the callback from getUserMedia
-  // so that Firefox doesn't eat the second request.
-  // this is the part that's complaining TODO
-  if (hasHtml5CameraSupport) {
-    navigator.getUserMedia({video: true}, function(stream) {
-      video[0].src = window.URL.createObjectURL(stream);
-      localMediaStream = stream;
-
-      // We do this in a recursive call on success because Firefox seems to
-      // simply eat the request if you stack up two on top of each other before
-      // the user has a chance to approve the first one.
-      initSnapshotHandler(names, hasHtml5CameraSupport);
-    }, onVideoFail);
-  }
-  else {
-    initSnapshotHandler(names, hasHtml5CameraSupport);
-  }
-
-}
-
-function browserHasFlash() {
-  var hasFlash = false;
-  try {
-      var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
-      if(fo) hasFlash = true;
-  } catch(e) {
-      if(navigator.mimeTypes["application/x-shockwave-flash"] != undefined) hasFlash = true;
-  }
-  return hasFlash;
-}
-
-function objectTagForFlashCamera(name) {
-  // detect whether or not flash is available
-  if(browserHasFlash()) {
-      // I manually update this to have ?v={2,3,4, etc} to avoid caching of flash
-      // objects on local dev.
-      return '<object type="application/x-shockwave-flash" id="' +
-             name + '" name="' + name + '" data=' +
-             '"/static/js/verify_student/CameraCapture.swf?v=3"' +
-              'width="500" height="375"><param name="quality" ' +
-              'value="high"><param name="allowscriptaccess" ' +
-              'value="sameDomain"></object>';
-  }
-  else {
-      // display a message informing the user to install flash
-      $('#no-flash').show();
-  }
-}
-
-function linkNewWindow(e) {
-        window.open($(e.target).attr('href'));
-            e.preventDefault();
-}
-
-function waitForFlashLoad(func, flash_object) {
-    if(!flash_object.hasOwnProperty('percentLoaded') || flash_object.percentLoaded() < 100){
-        setTimeout(function() {
-            waitForFlashLoad(func, flash_object);
-        },
-        50);
-    }
-    else {
-        func(flash_object);
-    }
-}
-
-$(document).ready(function() {
-  $(".carousel-nav").addClass('sr');
-  $("#pay_button").click(function(){
-      analytics.pageview("Payment Form");
-      submitToPaymentProcessing();
-  });
-
-  $("#reverify_button").click(function() {
-      submitReverificationPhotos();
-  });
-
-  // prevent browsers from keeping this button checked
-  $("#confirm_pics_good").prop("checked", false)
-  $("#confirm_pics_good").change(function() {
-      $("#pay_button").toggleClass('disabled');
-      $("#reverify_button").toggleClass('disabled');
-  });
-
-
-  // add in handlers to add/remove the correct classes to the body
-  // when moving between steps
-  $('#face_next_link').click(function(){
-      analytics.pageview("Capture ID Photo");
-      $('body').addClass('step-photos-id').removeClass('step-photos-cam')
-  })
-
-  $('#photo_id_next_link').click(function(){
-      analytics.pageview("Review Photos");
-      $('body').addClass('step-review').removeClass('step-photos-id')
-  })
-
-  // set up edit information dialog
-  $('#edit-name div[role="alert"]').hide();
-  $('#edit-name .action-save').click(submitNameChange);
-
-  var hasHtml5CameraSupport = initVideoCapture();
-
-  // If HTML5 WebRTC capture is not supported, we initialize jpegcam
-  if (!hasHtml5CameraSupport) {
-    $("#face_capture_div").html(objectTagForFlashCamera("face_flash"));
-    // wait for the flash object to be loaded and then check for a camera
-    if(browserHasFlash()) {
-        waitForFlashLoad(function(flash_object) {
-            if(!flash_object.hasOwnProperty('hasCamera')){
-                onVideoFail('NO_DEVICES_FOUND');
-            }
-        }, $('#face_flash')[0]);
-    }
-  }
-
-  analytics.pageview("Capture Face Photo");
-  initSnapshotHandler(["face"], hasHtml5CameraSupport);
-
-  $('a[rel="external"]').attr('title', gettext('This link will open in a new browser window/tab')).bind('click', linkNewWindow);
-
-});
diff --git a/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html b/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
index 51e0da34277..6b988d4618f 100644
--- a/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
+++ b/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
@@ -6,7 +6,7 @@
 <div class="msg msg-reverify has-actions">
     <div class="msg-content">
       <h2 class="title">${_("You need to re-verify to continue")}</h2>
-      % for course_id, course_name, date, status in reverify_course_data:
+      % for course_id, course_name, course_number, date, status in reverify_course_data:
       <div class="copy">
         <p class='activation-message'>
         	${_('To continue in the verified track in <strong>{course_name}</strong>, you need to re-verify your identity by {date}.').format(course_name=course_name, date=date)}
diff --git a/lms/templates/verify_student/midcourse_photo_reverification.html b/lms/templates/verify_student/midcourse_photo_reverification.html
index ac06e65234f..ce9c9e24c94 100644
--- a/lms/templates/verify_student/midcourse_photo_reverification.html
+++ b/lms/templates/verify_student/midcourse_photo_reverification.html
@@ -9,7 +9,7 @@
 <%block name="js_extra">
 <script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
 <script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
-<script src="${static.url('js/verify_student/photocapturebasic2.js')}"></script>
+<script src="${static.url('js/verify_student/photocapture.js')}"></script>
 </%block>
 
 
@@ -176,7 +176,7 @@
                     <form id="reverify_form" method="post">
                       <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
                       <input type="hidden" name="course_id" value="${course_id}">
-                      <input class="action-primary disabled" type="button" id="reverify_button" value="Submit photos &amp; re-verify" name="payment">
+                      <input class="action-primary disabled" type="button" id="midcourse_reverify_button" value="Submit photos &amp; re-verify" name="payment">
                     </form>
                   </li>
                 </ol>
diff --git a/lms/templates/verify_student/midcourse_reverify_dash.html b/lms/templates/verify_student/midcourse_reverify_dash.html
index a05f1e7211b..dd01ac67664 100644
--- a/lms/templates/verify_student/midcourse_reverify_dash.html
+++ b/lms/templates/verify_student/midcourse_reverify_dash.html
@@ -25,7 +25,7 @@
           <th>Status</th>
           </tr>
 
-          % for course_id, course_name, date, status in reverify_course_data:
+          % for course_id, course_name, course_number, date, status in reverify_course_data:
             <tr>
             	<td>
                 <span class="course-name">${course_name} (HKS211.1x)</span>
diff --git a/lms/templates/verify_student/reverification_window_expired.html b/lms/templates/verify_student/reverification_window_expired.html
new file mode 100644
index 00000000000..b113f8d44f3
--- /dev/null
+++ b/lms/templates/verify_student/reverification_window_expired.html
@@ -0,0 +1,45 @@
+
+<%! from django.utils.translation import ugettext as _ %>
+<%! from django.core.urlresolvers import reverse %>
+<%inherit file="../main.html" />
+<%namespace name='static' file='/static_content.html'/>
+
+<%block name="bodyclass">register verification-process is-not-verified step-confirmation</%block>
+<%block name="title"><title>${_("Re-Verification Failed")}</title></%block>
+
+<%block name="js_extra">
+<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
+<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
+</%block>
+<%block name="content">
+
+<div class="container">
+  <section class="wrapper">
+
+    <div class="wrapper-content-main">
+      <article class="content-main">
+        <section class="content-confirmation">
+          <div class="wrapper-view">
+            <div class="view">
+              <h3 class="title">${_("Re-Verification Failed")}</h3>
+
+              <div class="instruction">
+                <p>${_("Your re-verification was submitted after the re-verification deadline, and you can no longer be re-verified.")}</p>
+                <p>${_("Please contact support if you believe this message to be in error.")}</p>
+              </div>
+
+              <ol class="list-nav">
+                <li class="nav-item">
+                  <a class="action action-primary" href="${reverse('dashboard')}">${_("Return to Your Dashboard")}</a>
+                </li>
+              </ol>
+            </div> <!-- /view -->
+          </div> <!-- /wrapper-view -->
+        </section>
+      </article>
+    </div> <!-- /wrapper-content-main -->
+
+    <%include file="_reverification_support.html" />
+  </section>
+</div>
+</%block>
-- 
GitLab