From 85030467ed0f24797599212d9bc5b84a4c1bedae Mon Sep 17 00:00:00 2001
From: Julia Hansbrough <julia@edx.org>
Date: Mon, 13 Jan 2014 20:10:32 +0000
Subject: [PATCH] Skeleton for follow-on verification behavior

Quick-and-dirty end-to-end flow, functional for at least the case of one course in need of re-verification.

Ready for design to start taking a look; still needs tests and code cleanup
---
 common/djangoapps/student/views.py            |  26 +-
 lms/djangoapps/certificates/queue.py          |  13 +-
 lms/djangoapps/verify_student/admin.py        |   2 +
 ...auto__add_midcoursereverificationwindow.py |  91 +++++
 ...03_auto__add_sspmidcoursereverification.py |  94 +++++
 lms/djangoapps/verify_student/models.py       | 273 ++++++++++++++-
 lms/djangoapps/verify_student/urls.py         |  18 +
 lms/djangoapps/verify_student/views.py        |  91 ++++-
 .../js/verify_student/photocapturebasic2.js   | 322 ++++++++++++++++++
 lms/templates/dashboard.html                  |   7 +
 .../_dashboard_prompt_midcourse_reverify.html |  13 +
 .../midcourse_photo_reverification.html       | 201 +++++++++++
 ...midcourse_reverification_confirmation.html |  45 +++
 .../midcourse_reverify_dash.html              |  28 ++
 .../prompt_midcourse_reverify.html            |   6 +
 15 files changed, 1219 insertions(+), 11 deletions(-)
 create mode 100644 lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow.py
 create mode 100644 lms/djangoapps/verify_student/migrations/0003_auto__add_sspmidcoursereverification.py
 create mode 100644 lms/static/js/verify_student/photocapturebasic2.js
 create mode 100644 lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
 create mode 100644 lms/templates/verify_student/midcourse_photo_reverification.html
 create mode 100644 lms/templates/verify_student/midcourse_reverification_confirmation.html
 create mode 100644 lms/templates/verify_student/midcourse_reverify_dash.html
 create mode 100644 lms/templates/verify_student/prompt_midcourse_reverify.html

diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 5ab43a161d8..ea0c2a5114e 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -10,6 +10,8 @@ import string       # pylint: disable=W0402
 import urllib
 import uuid
 import time
+import datetime
+from pytz import UTC
 
 from django.conf import settings
 from django.contrib.auth import logout, authenticate, login
@@ -45,7 +47,7 @@ from student.models import (
 )
 from student.forms import PasswordResetFormNoActive
 
-from verify_student.models import SoftwareSecurePhotoVerification
+from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification
 from certificates.models import CertificateStatuses, certificate_status_for_student
 
 from xmodule.course_module import CourseDescriptor
@@ -205,6 +207,7 @@ def _cert_info(user, course, cert_status):
         CertificateStatuses.restricted: 'restricted',
     }
 
+    # TODO: We need the thing on the sidebar to mention if reverification, as per UI flows.
     status = template_state.get(cert_status['status'], default_status)
 
     d = {'status': status,
@@ -386,8 +389,27 @@ def dashboard(request):
     )
 
     # Verification Attempts
+    # Used to generate the "you must reverify for course x" banner
+    # 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...
+    prompt_midcourse_reverify = False
+    reverify_course_data = []
+    for (course, enrollment) in course_enrollment_pairs:
+        if MidcourseReverificationWindow.window_open_for_course(course.id) and not SSPMidcourseReverification.user_has_valid_or_pending(user, course.id):
+            window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
+            status_for_window = SSPMidcourseReverification.get_status_for_window(user, 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)
+                )
+            )
+            prompt_midcourse_reverify = True
+
     show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
                                        if _enrollment.refundable())
 
@@ -408,6 +430,8 @@ def dashboard(request):
                'all_course_modes': course_modes,
                'cert_statuses': cert_statuses,
                'show_email_settings_for': show_email_settings_for,
+               'prompt_midcourse_reverify': prompt_midcourse_reverify,
+               'reverify_course_data': reverify_course_data,
                'verification_status': verification_status,
                'verification_msg': verification_msg,
                'show_refund_option_for': show_refund_option_for,
diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py
index 2f9e70517a1..0cf92ba4286 100644
--- a/lms/djangoapps/certificates/queue.py
+++ b/lms/djangoapps/certificates/queue.py
@@ -179,11 +179,18 @@ class XQueueCertInterface(object):
             org = course_id.split('/')[0]
             course_num = course_id.split('/')[1]
             cert_mode = enrollment_mode
-            if enrollment_mode == GeneratedCertificate.MODES.verified and SoftwareSecurePhotoVerification.user_is_verified(student):
+            if (
+                (enrollment_mode == GeneratedCertificate.MODES.verified) and 
+                SoftwareSecurePhotoVerification.user_is_verified(student) and
+                SSPMidcourseReverification.user_is_reverified_for_all(course_id, student)
+            ):
                 template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
                     org, course_num)
-            elif (enrollment_mode == GeneratedCertificate.MODES.verified and not
-                    SoftwareSecurePhotoVerification.user_is_verified(student)):
+            elif (
+                (enrollment_mode == GeneratedCertificate.MODES.verified) and not
+                (SoftwareSecurePhotoVerification.user_is_verified(student)) and not
+                (SSPMidcourseReverification.user_is_reverified_for_all(course_id, student))
+            ):
                 template_pdf = "certificate-template-{0}-{1}.pdf".format(
                     org, course_num)
                 cert_mode = GeneratedCertificate.MODES.honor
diff --git a/lms/djangoapps/verify_student/admin.py b/lms/djangoapps/verify_student/admin.py
index 9f96ca920b6..fe7c2411a7d 100644
--- a/lms/djangoapps/verify_student/admin.py
+++ b/lms/djangoapps/verify_student/admin.py
@@ -1,4 +1,6 @@
 from ratelimitbackend import admin
 from verify_student.models import SoftwareSecurePhotoVerification
+from verify_student.models import MidcourseReverificationWindow
 
 admin.site.register(SoftwareSecurePhotoVerification)
+admin.site.register(MidcourseReverificationWindow)
\ No newline at end of file
diff --git a/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow.py b/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow.py
new file mode 100644
index 00000000000..5543247aed3
--- /dev/null
+++ b/lms/djangoapps/verify_student/migrations/0002_auto__add_midcoursereverificationwindow.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'MidcourseReverificationWindow'
+        db.create_table('verify_student_midcoursereverificationwindow', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
+            ('start_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
+            ('end_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
+        ))
+        db.send_create_signal('verify_student', ['MidcourseReverificationWindow'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'MidcourseReverificationWindow'
+        db.delete_table('verify_student_midcoursereverificationwindow')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'verify_student.midcoursereverificationwindow': {
+            'Meta': {'object_name': 'MidcourseReverificationWindow'},
+            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
+        },
+        'verify_student.softwaresecurephotoverification': {
+            'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
+            'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
+            'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1fdbb90>'", 'max_length': '255', 'db_index': 'True'}),
+            'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
+            'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
+            'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+            'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        }
+    }
+
+    complete_apps = ['verify_student']
\ No newline at end of file
diff --git a/lms/djangoapps/verify_student/migrations/0003_auto__add_sspmidcoursereverification.py b/lms/djangoapps/verify_student/migrations/0003_auto__add_sspmidcoursereverification.py
new file mode 100644
index 00000000000..55720b1d044
--- /dev/null
+++ b/lms/djangoapps/verify_student/migrations/0003_auto__add_sspmidcoursereverification.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'SSPMidcourseReverification'
+        db.create_table('verify_student_sspmidcoursereverification', (
+            ('softwaresecurephotoverification_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['verify_student.SoftwareSecurePhotoVerification'], unique=True, primary_key=True)),
+            ('window', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.MidcourseReverificationWindow'])),
+        ))
+        db.send_create_signal('verify_student', ['SSPMidcourseReverification'])
+
+
+    def backwards(self, orm):
+        # Deleting model 'SSPMidcourseReverification'
+        db.delete_table('verify_student_sspmidcoursereverification')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'verify_student.midcoursereverificationwindow': {
+            'Meta': {'object_name': 'MidcourseReverificationWindow'},
+            'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+            'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
+        },
+        'verify_student.softwaresecurephotoverification': {
+            'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
+            'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+            'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+            'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
+            'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
+            'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1189320>'", 'max_length': '255', 'db_index': 'True'}),
+            'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
+            'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
+            'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+            'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'verify_student.sspmidcoursereverification': {
+            'Meta': {'ordering': "['-created_at']", 'object_name': 'SSPMidcourseReverification', '_ormbases': ['verify_student.SoftwareSecurePhotoVerification']},
+            'softwaresecurephotoverification_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'unique': 'True', 'primary_key': 'True'}),
+            'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.MidcourseReverificationWindow']"})
+        }
+    }
+
+    complete_apps = ['verify_student']
\ No newline at end of file
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index dccbdb430ae..a93362d9c9f 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -23,6 +23,7 @@ import pytz
 import requests
 
 from django.conf import settings
+from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse
 from django.db import models
 from django.contrib.auth.models import User
@@ -37,6 +38,57 @@ from verify_student.ssencrypt import (
 
 log = logging.getLogger(__name__)
 
+# Evidently South migrations complain a lot if you have a default set to uuid.uuid4, so
+# I had to add this function to make South happy, see this for more:
+#   http://stackoverflow.com/questions/15041265/south-migrate-error-name-uuid-is-not-defined
+# If anyone knows a happier solution, do let me know; otherwise I'll remove this comment
+# after CR
+def generateUUID():
+        return str(uuid.uuid4)
+
+class MidcourseReverificationWindow(models.Model):
+    """
+    Defines the start and end times for midcourse reverification for a particular course.
+
+    There can be many MidcourseReverificationWindows per course, but they should not
+    have overlapping time-ranges (i.e. Window2's start date should not be before Window1's
+    start date) (TODO: should the non-overlap constraint be explicitly enforced by the model?)
+    """
+    # the course that this window is attached to
+    # TODO should this be a foreignkey?
+    course_id = models.CharField(max_length=255, db_index=True)
+    start_date = models.DateTimeField(default=None, null=True, blank=True)
+    end_date = models.DateTimeField(default=None, null=True, blank=True)
+
+    @classmethod
+    def window_open_for_course(cls, course_id):
+        """
+        Returns a boolean, True if the course is currently asking for reverification, else False.
+        """
+        now = datetime.now(pytz.UTC)
+
+        # We are assuming one window per course_id.  TODO find out if this assumption is OK
+        try:
+            window = cls.objects.get(course_id=course_id)
+        except(ObjectDoesNotExist):
+            return False
+
+        if (window.start_date <= now <= window.end_date):
+            return True
+        else:
+            return False
+
+    @classmethod
+    def get_window(cls, course_id, date):
+        """
+        Returns the window that is open for a particular course for a particular date.
+        If no such window is open, or if more than one window is open, returns None.
+        """
+        try:
+            return cls.objects.get(course_id=course_id, start_date__lte=date, end_date__gte=date)
+        except Exception:
+            return None
+
 
 class VerificationException(Exception):
     pass
@@ -135,7 +187,7 @@ class PhotoVerification(StatusModel):
     # user IDs or something too easily guessable.
     receipt_id = models.CharField(
         db_index=True,
-        default=uuid.uuid4,
+        default=generateUUID,
         max_length=255,
     )
 
@@ -167,6 +219,8 @@ class PhotoVerification(StatusModel):
     # capturing it so that we can later query for the common problems.
     error_code = models.CharField(blank=True, max_length=50)
 
+    
+
     class Meta:
         abstract = True
         ordering = ['-created_at']
@@ -483,7 +537,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"]
         aes_key = aes_key_str.decode("hex")
 
-        s3_key = self._generate_key("face")
+        s3_key = self._generate_s3_key("face")
         s3_key.set_contents_from_string(encrypt_and_encode(img_data, aes_key))
 
     @status_before_must_be("created")
@@ -510,7 +564,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         rsa_encrypted_aes_key = rsa_encrypt(aes_key, rsa_key_str)
 
         # Upload this to S3
-        s3_key = self._generate_key("photo_id")
+        s3_key = self._generate_s3_key("photo_id")
         s3_key.set_contents_from_string(encrypt_and_encode(img_data, aes_key))
 
         # Update our record fields
@@ -580,11 +634,13 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         We dynamically generate this, since we want it the expiration clock to
         start when the message is created, not when the record is created.
         """
-        s3_key = self._generate_key(name)
+        s3_key = self._generate_s3_key(name)
         return s3_key.generate_url(self.IMAGE_LINK_DURATION)
 
-    def _generate_key(self, prefix):
+    def _generate_s3_key(self, prefix):
         """
+        Generates a key for an s3 bucket location
+
         Example: face/4dd1add9-6719-42f7-bea0-115c008c4fca
         """
         conn = S3Connection(
@@ -689,3 +745,210 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         log.debug("Return message:\n\n{}\n\n".format(response.text))
 
         return response
+
+class SSPMidcourseReverification(SoftwareSecurePhotoVerification):
+    """
+    Model to re-verify identity using a service provided by Software Secure. 
+
+    As of now, it's inheriting a great deal of logic from both `PhotoVerification`
+    and `SoftwareSecurePhotoVerification`, but it might make more sense to just inherit
+    from `PhotoVerification`, or maybe not at all... a lot of classes had to get stomped/
+    rewritten.  Will think about this during CR.
+
+    TODO: another important thing to note during CR: right now we're assuming there's one
+    window per (user, course) combo.  This is UNTRUE in general (there can be many windows
+    per course, user pair), but we only need ONE window per (user, course) to launch.
+    Note the user_status methods in particular make this assumption.
+
+    Fix this if time permits...
+    """
+    window = models.ForeignKey(MidcourseReverificationWindow, db_index=True)
+
+    @classmethod
+    def user_is_reverified_for_all(self, course_id, user):
+        """
+        Checks to see if the student has successfully reverified for all of the
+        mandatory re-verification windows associated with a course.
+
+        This is used primarily by the certificate generation code... if the user is
+        not re-verified for all windows, then they cannot receive a certificate.
+        """
+        all_windows = MidcourseReverificationWindow.objects.filter(course_id=course_id)
+        # TODO check on this
+        # if there are no windows for a course, then return True right off
+        if (not all_windows):
+            return True
+        for window in all_windows:
+            try:
+                # There should be one and only one reverification object per (user, window)
+                # and the status of that object should be approved
+                if cls.objects.get(window=window, user=user).status != "approved":
+                    return False
+            except:
+                return False
+        return True
+
+    # TODO does this actually get the original_verification? pretty sure I need to search by date
+    def original_verification(self):
+        """
+        Returns the most current SoftwareSecurePhotoVerification object associated with the user. 
+        """
+        return (SoftwareSecurePhotoVerification.objects.get(user=self.user))
+
+    # TODO could just call original_verification's _generate_s3_key?
+    def _generate_original_s3_key(self, prefix):
+        
+        #Generates a key into the S3 bucket where the original verification is stored
+
+        #Example: face/4dd1add9-6719-42f7-bea0-115c008c4fca
+        
+        conn = S3Connection(
+            settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["AWS_ACCESS_KEY"],
+            settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["AWS_SECRET_KEY"]
+        )
+        bucket = conn.get_bucket(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["S3_BUCKET"])
+
+        key = Key(bucket)
+        key.key = "{}/{}".format(prefix, self.original_verification().receipt_id)
+
+        return key
+
+    @status_before_must_be("created")
+    def fetch_photo_id_image(self):
+        
+        #Find the user's photo ID image, which was submitted with their original verification.
+        #The image has already been encrypted and stored in s3, so we just need to find that
+        #location
+        
+
+        if settings.FEATURES.get('AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'):
+            return
+
+        old_s3_key = self._generate_original_s3_key("face")
+        new_s3_key = self._generate_s3_key("face")
+
+        original_photo_id = old_s3_key.get_contents_as_string()
+
+        # Unlike upload_face_image, we don't need to encrypt and encode with AES, since that
+        # was already done when we uploaded it for the initial verification
+        new_s3_key.set_contents_from_string(original_photo_id)
+        self.photo_id_key = self.original_verification().photo_id_key
+        self.save()
+
+    # we replace_photo_id_image with fetch_photo_id_image
+    @status_before_must_be("created")
+    def upload_photo_id_image(self, img_data):
+        raise NotImplementedError
+
+    # TODO right now this does nothing but return must_reverify, fix!!!
+    @classmethod
+    def get_status_for_window(cls, user, window):
+        """
+        Returns the status of the user based on their past verification attempts
+
+        If no such verification exists, returns 'must_reverify'
+        If verification has expired, returns 'expired' --> does this exist for windows?
+        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'
+        """
+        reverify_attempt = cls.objects.filter(user=user, window=window)
+        return "must_reverify"
+        #if not reverify_attempt:
+        #    return "must_reverify"
+        #else:
+            #return reverify_attempt.STATUS
+
+    # can't just inherit the old user_status function, because it's insufficiently specific
+    # reverifications are unique for a particular (user, window) pair, not just on user
+    # TODO: Note that a lot of the user_status related stuff is having to get overwritten.
+    # Does it still make sense to inherit from our parent object(s)?
+    @classmethod
+    def user_status(cls, user):
+        raise NotImplementedError
+
+    @classmethod
+    def user_status(cls, user, course_id):
+        """
+        Returns the status of the user based on their past verification attempts
+
+        If no such verification exists, returns 'none'
+        If verification has expired, returns 'expired'
+        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'
+        """
+        status = 'none'
+        error_msg = ''
+
+        if cls.user_is_verified(user):
+            status = 'approved'
+        elif cls.user_has_valid_or_pending(user):
+            # user_has_valid_or_pending does include 'approved', but if we are
+            # here, we know that the attempt is still pending
+            status = 'pending'
+        else:
+            # we need to check the most recent attempt to see if we need to ask them to do
+            # a retry
+            try:
+                attempts = cls.objects.filter(user=user).order_by('-updated_at')
+                attempt = attempts[0]
+            # this is the change for SSPMidcoursePhotoVerification objects
+            # if there is no verification, we look up course_id, via window, and find out if the user has a verified enrollment
+            # if verified enrolled in course but no verification: must_reverify
+            # if not verified enrollment: none
+            except IndexError:
+                if CourseEnrollment.objects.filter(user=user, course_id=course_id, mode="verified").exists:
+                    return ('must_reverify', error_msg)
+                else:
+                    return('none', error_msg)
+            if attempt.created_at < cls._earliest_allowed_date():
+                return ('expired', error_msg)
+
+            # right now, this is the only state at which they must reverify. It
+            # may change later
+            if attempt.status == 'denied':
+                status = 'must_reverify'
+            if attempt.error_msg:
+                error_msg = attempt.parsed_error_msg()
+
+        return (status, error_msg)
+
+    # can't inherit
+    @classmethod
+    def user_is_verified(cls, user):
+        raise NotImplementedError
+
+    @classmethod
+    def user_is_verified(cls, user, course_id):
+        return cls.objects.filter(
+            user=user, status="approved", window__course_id=course_id
+        ).exists()
+
+    # can't inherit
+    @classmethod
+    def user_has_valid_or_pending(cls, user):
+        return NotImplementedError
+
+    # changing this method?
+    @classmethod
+    def user_has_valid_or_pending(cls, user, course_id):
+        valid_statuses = ['submitted', 'approved']
+        return cls.objects.filter(
+            user=user,
+            window__course_id=course_id,
+            status__in=valid_statuses,
+        ).exists()
+
+    # can't inherit
+    @classmethod
+    def active_for_user(cls, user):
+        return NotImplementedError
+
+    @classmethod
+    def active_for_user(cls, user, course_id):
+        active_attempts = cls.objects.filter(user=user, status='ready', window__course_id=course_id)
+        if active_attempts:
+            return active_attempts[0]
+        else:
+            return None
diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py
index 843ebf96028..40b434ecea1 100644
--- a/lms/djangoapps/verify_student/urls.py
+++ b/lms/djangoapps/verify_student/urls.py
@@ -41,9 +41,27 @@ urlpatterns = patterns(
         name="verify_student_reverify"
     ),
 
+    url(
+        r'^midcourse_reverify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
+        views.MidCourseReverifyView.as_view(),
+        name="verify_student_midcourse_reverify"
+    ),
+
     url(
         r'^reverification_confirmation$',
         views.reverification_submission_confirmation,
         name="verify_student_reverification_confirmation"
     ),
+
+    url(
+        r'^midcourse_reverification_confirmation$',
+        views.midcourse_reverification_confirmation,
+        name="verify_student_midcourse_reverification_confirmation"
+    ),
+
+    url(
+        r'^midcourse_reverify_dash$',
+        views.midcourse_reverify_dash,
+        name="verify_student_midcourse_reverify_dash"
+    ),
 )
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 2ac798bbf20..d972441d4dd 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -5,6 +5,8 @@ Views for the verification flow
 import json
 import logging
 import decimal
+import datetime
+from pytz import UTC
 
 from edxmako.shortcuts import render_to_response
 
@@ -27,7 +29,9 @@ from shoppingcart.models import Order, CertificateItem
 from shoppingcart.processors.CyberSource import (
     get_signed_purchase_params, get_purchase_endpoint
 )
-from verify_student.models import SoftwareSecurePhotoVerification
+from verify_student.models import (
+    SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification
+)
 import ssencrypt
 
 log = logging.getLogger(__name__)
@@ -322,11 +326,94 @@ class ReverifyView(View):
             }
             return render_to_response("verify_student/photo_reverification.html", context)
 
+class MidCourseReverifyView(View):
+    """
+    The mid-course reverification view.
+    Needs to perform these functions:
+        - take new face photo
+        - retrieve the old id photo
+        - submit these photos to photo verification service
+
+    Does not need to worry about pricing
+    """
+    @method_decorator(login_required)
+    def get(self, request, course_id):
+        """
+        display this view
+        """
+        context = {
+            "user_full_name": request.user.profile.name,
+            "error": False,
+            "course_id": course_id,
+        }
+        return render_to_response("verify_student/midcourse_photo_reverification.html", context)
+
+    @method_decorator(login_required)
+    def post(self, request, course_id):
+        """
+        submits the reverification to SoftwareSecure
+        """
+        try:
+            # TODO look at this more carefully! #1 testing candidate
+            now = datetime.datetime.now(UTC)
+            attempt = SSPMidcourseReverification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now))
+            b64_face_image = request.POST['face_image'].split(",")[1]
+
+            attempt.upload_face_image(b64_face_image.decode('base64'))
+            attempt.fetch_photo_id_image()
+            attempt.mark_ready()
+
+            attempt.save()
+            attempt.submit()
+            return HttpResponseRedirect(reverse('verify_student_midcourse_reverification_confirmation'))
+        except Exception:
+            log.exception(
+                "Could not submit verification attempt for user {}".format(request.user.id)
+            )
+            context = {
+                "user_full_name": request.user.profile.name,
+                "error": True,
+            }
+            return render_to_response("verify_student/midcourse_photo_reverification.html", context)
+
+def midcourse_reverify_dash(_request):
+    # 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):
+        try:
+            course_enrollment_pairs.append((course_from_id(enrollment.course_id), enrollment))
+        except ItemNotFoundError:
+            log.error("User {0} enrolled in non-existent course {1}"
+                      .format(user.username, enrollment.course_id))
+    reverify_course_data = []
+    for (course, enrollment) in course_enrollment_pairs:
+        if MidcourseReverificationWindow.window_open_for_course(course.id):
+            reverify_course_data.append(
+                (
+                    course.id,
+                    course.display_name,
+                    MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC)).end_date,
+                    "must_reverify"
+                )
+            )
+            prompt_midcourse_reverify = True
+    context = {
+        "user_full_name": _request.user.profile.name,
+        "reverify_course_data": reverify_course_data,
+    }
+    return render_to_response("verify_student/midcourse_reverify_dash.html", context)
 
 @login_required
 def reverification_submission_confirmation(_request):
     """
     Shows the user a confirmation page if the submission to SoftwareSecure was successful
     """
-
     return render_to_response("verify_student/reverification_confirmation.html")
+
+@login_required
+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")
diff --git a/lms/static/js/verify_student/photocapturebasic2.js b/lms/static/js/verify_student/photocapturebasic2.js
new file mode 100644
index 00000000000..0ca03aca4de
--- /dev/null
+++ b/lms/static/js/verify_student/photocapturebasic2.js
@@ -0,0 +1,322 @@
+// TODO diff this against photocapture.js, see if I actually needed a whole honking new file
+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");
+
+    $("#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,
+    },
+    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.html b/lms/templates/dashboard.html
index 33470d1d0be..397c73fff07 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -154,6 +154,13 @@
 
 <section class="container dashboard" id="dashboard-main" aria-hidden="false">
 
+  <!-- TODO later will need to make this ping for all courses on the dash -->
+  %if prompt_midcourse_reverify:
+    <section class="dashboard-banner">
+      <%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
+    </section>
+  % endif
+
   %if message:
     <section class="dashboard-banner">
       ${message}
diff --git a/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html b/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
new file mode 100644
index 00000000000..b0c3bb8fa0f
--- /dev/null
+++ b/lms/templates/dashboard/_dashboard_prompt_midcourse_reverify.html
@@ -0,0 +1,13 @@
+<%! from django.utils.translation import ugettext as _ %>
+<%! from django.core.urlresolvers import reverse %>
+<!--TODO replace this with something a clever deisgn person approves of-->
+<!--TODO replace this with a shiny loopy thing to actually print out all courses-->
+% if prompt_midcourse_reverify:
+<h2>${_("You need to re-verify to continue")}</h2>
+% for course_id, course_name, date, status in reverify_course_data:
+<p class='activation-message'>
+	${_('To continue in the verified track in {course_name}, you need to re-verify your identity by {date}.').format(course_name=course_name, date=date)}
+	<a href="${reverse('verify_student_midcourse_reverify_dash')}">Click here to re-verify.</a>
+</p>
+% endfor
+%endif
diff --git a/lms/templates/verify_student/midcourse_photo_reverification.html b/lms/templates/verify_student/midcourse_photo_reverification.html
new file mode 100644
index 00000000000..98c464329d2
--- /dev/null
+++ b/lms/templates/verify_student/midcourse_photo_reverification.html
@@ -0,0 +1,201 @@
+<%! 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-photos</%block>
+<%block name="title"><title>${_("Re-Verification")}</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>
+<script src="${static.url('js/verify_student/photocapturebasic2.js')}"></script>
+</%block>
+
+
+<%block name="content">
+
+<div id="no-webcam" style="display: none;" class="wrapper-msg wrapper-msg-activate">
+  <div class=" msg msg-activate">
+    <i class="msg-icon icon-warning-sign"></i>
+    <div class="msg-content">
+      <h3 class="title">${_("No Webcam Detected")}</h3>
+      <div class="copy">
+        <p>${_("You don't seem to have a webcam connected. Double-check that your webcam is connected and working to continue.")}</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div id="no-flash" style="display: none;" class="wrapper-msg wrapper-msg-activate">
+  <div class=" msg msg-activate">
+    <i class="msg-icon icon-warning-sign"></i>
+    <div class="msg-content">
+      <h3 class="title">${_("No Flash Detected")}</h3>
+      <div class="copy">
+        <p>${_("You don't seem to have Flash installed. {a_start} Get Flash {a_end} to continue your registration.").format(a_start='<a rel="external" href="http://get.adobe.com/flashplayer/">', a_end="</a>")}</p>
+      </div>
+    </div>
+  </div>
+</div>
+
+%if error:
+<div id="submission-error" class="wrapper-msg wrapper-msg-activate">
+  <div class=" msg msg-activate">
+    <i class="msg-icon icon-warning-sign"></i>
+    <div class="msg-content">
+      <h3 class="title">${_("Error submitting your images")}</h3>
+      <div class="copy">
+        <p>${_("Oops! Something went wrong. Please confirm your details and try again.")}</p>
+      </div>
+    </div>
+  </div>
+</div>
+%endif
+
+<div class="container">
+  <section class="wrapper">
+
+    <div class="wrapper-reverification">
+      <section class="reverification">
+        <div class="message">
+          <h3 class="title">${_("You are re-verifying your identity")}</h3>
+        </div>
+
+        <span class="deco-arrow"></span>
+      </section>
+    </div>
+
+    <div class="wrapper-content-main">
+      <article class="content-main">
+
+        <section class="wrapper carousel"  data-transition="slide">
+          <div id="wrapper-facephoto" class="wrapper-view block-photo">
+            <div class="facephoto view">
+              <h3 class="title">${_("Re-Take Your Photo")}</h3>
+              <div class="instruction">
+                <p>${_("Use your webcam to take a picture of your face so we can match it with your original verification.")}</p>
+              </div>
+
+              <div class="wrapper-task">
+                <div id="facecam" class="task cam">
+                  <div class="placeholder-cam" id="face_capture_div">
+
+                    <div class="placeholder-art">
+                      <p class="copy">${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission.")}</p>
+                    </div>
+
+                    <video id="face_video" autoplay></video><br/>
+                    <canvas id="face_canvas" style="display:none;" width="640" height="480"></canvas>
+                  </div>
+
+                  <div class="controls photo-controls">
+                    <ul class="list-controls">
+                      <li class="control control-redo" id="face_reset_button">
+                        <a class="action action-redo" href="">
+                          <i class="icon-undo"></i> <span class="sr">${_("Retake")}</span>
+                        </a>
+                      </li>
+
+                      <li class="control control-do" id="face_capture_button">
+                        <a class="action action-do" href="">
+                          <i class="icon-camera"></i><span class="sr">${_("Take photo")}</span>
+                        </a>
+                      </li>
+
+                      <li class="control control-approve" id="face_approve_button">
+                        <a class="action action-approve" href="">
+                          <i class="icon-ok"></i> <span class="sr">${_("Looks good")}</span>
+                        </a>
+                      </li>
+                    </ul>
+                  </div>
+                </div>
+
+                <div class="wrapper-help">
+                  <div class="help help-task photo-tips facetips">
+                    <h4 class="title">${_("Tips on taking a successful photo")}</h4>
+
+                    <div class="copy">
+                      <ul class="list-help">
+                        <li class="help-item">${_("Make sure your face is well-lit")}</li>
+                        <li class="help-item">${_("Be sure your entire face is inside the frame")}</li>
+                        <li class="help-item">${_("Can we match the photo you took with the one on your ID?")}</li>
+                        <li class="help-item">${_("Once in position, use the camera button")} <span class="example">(<i class="icon-camera"></i>)</span> ${_("to capture your picture")}</li>
+                        <li class="help-item">${_("Use the checkmark button")} <span class="example">(<i class="icon-ok"></i>)</span> ${_("once you are happy with the photo")}</li>
+                      </ul>
+                    </div>
+                  </div>
+
+                  <div class="help help-faq facefaq">
+                    <h4 class="sr title">${_("Common Questions")}</h4>
+
+                    <div class="copy">
+                      <dl class="list-faq">
+                        <dt class="faq-question">${_("Why do you need my photo?")}</dt>
+                        <dd class="faq-answer">${_("As part of the verification process, we need your photo to confirm that you are you.")}</dd>
+
+                        <dt class="faq-question">${_("What do you do with this picture?")}</dt>
+                        <dd class="faq-answer">${_("We only use it to verify your identity.  It is not displayed anywhere.")}</dd>
+                      </dl>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+
+                  <li class="review-task review-task-name">
+                    <h4 class="title">${_("Check Your Name")}</h4>
+
+                    <div class="copy">
+                        <p>${_("Make sure your full name on your edX account ({full_name}) matches the ID you originally submitted. We will also use this as the name on your certificate.").format(full_name="<span id='full-name'>" + user_full_name + "</span>")}</p>
+                    </div>
+
+                    <ul class="list-actions">
+                      <li class="action action-editname">
+                        <a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a>
+                      </li>
+                    </ul>
+                  </li>
+                </ol>
+              </div>
+
+              <!-- TODO janky -->
+              <img id="face_image" src="" style="visibility:hidden;"/>
+
+              <nav class="nav-wizard" id="face_id_next_button_nav">
+                <div class="prompt-verify">
+                  <h3 class="title">Before proceeding, please review carefully</h3>
+
+                   <p class="copy"> ${_("Once you verify your photo looks good and your name is correct, you can finish your re-verification and return to your course.  You will not have another chance to re-verify.")}</p>
+
+                  <ul class="list-actions">
+                    <li class="action action-verify">
+                      <input type="checkbox" name="match" id="confirm_pics_good" />
+                      <label for="confirm_pics_good">${_("Yes! You can confirm my identity with this information.")}</label>
+                    </li>
+                  </ul>
+                </div>
+
+                <ol class="wizard-steps">
+                  <li class="wizard-step step-proceed">
+                    <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 & re-verify" name="payment">
+                    </form>
+                  </li>
+                </ol>
+              </nav>
+            </div> <!-- /view -->
+          </div> <!-- /wrapper-view -->
+        </section>
+      </article>
+    </div> <!-- /wrapper-content-main -->
+
+    <%include file="_reverification_support.html" />
+  </section>
+</div>
+
+<%include file="_modal_editname.html" />
+</%block>
diff --git a/lms/templates/verify_student/midcourse_reverification_confirmation.html b/lms/templates/verify_student/midcourse_reverification_confirmation.html
new file mode 100644
index 00000000000..31f6e765387
--- /dev/null
+++ b/lms/templates/verify_student/midcourse_reverification_confirmation.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 Submission Confirmation")}</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">${_("Your Credentials Have Been Updated")}</h3>
+
+              <div class="instruction">
+                <p>${_("We have received your re-verification details and submitted them for review. Your dashboard will show the notification status once the review is complete.")}</p>
+                <p>${_("The professor may ask you to re-verify again at other key points in the course.")}</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>
diff --git a/lms/templates/verify_student/midcourse_reverify_dash.html b/lms/templates/verify_student/midcourse_reverify_dash.html
new file mode 100644
index 00000000000..92403c297da
--- /dev/null
+++ b/lms/templates/verify_student/midcourse_reverify_dash.html
@@ -0,0 +1,28 @@
+<%! from django.core.urlresolvers import reverse %>
+
+<h1>Re-verify</h1>
+
+<p>You currently need to re-verify for the following course:</p>
+
+% for course_id, course_name, date, status in reverify_course_data:
+	<p>${course_name}: Re-verify by ${date}. 
+		% if status == "must_reverify":
+		  <a href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': course_id})}">Re-verify</a>
+		% elif status == "completed":
+		  Completed
+		% elif status == "failed":
+		  Failed
+		% endif
+	</p>
+% endfor
+
+<h2>Why do I need to re-verify?</h2>
+<p>At key points in a course, the professor will ask you to re-verify your identity by submitting a new photo of your face. We will send the new photo to be matched up with the photo of the original ID you submitted when you signed up for the course. If you are taking multiple courses, you may need to re-verify multiple times, once for every important point in each course you are taking as a verified student.</p>
+
+<h2>What will I need to re-verify?</h2>
+<p>Because you are just confirming that you are still you, the only thing you will need to do to re-verify is to <b>submit a new photo of your face with your webcam</b>. The process is quick and you will be brought back to where you left off so you can keep on learning.</p>
+
+<p>If you changed your name during the semester and it no longer matches the original ID you submitted, you will need to re-edit your name to match as well.</p>
+
+<h2>What if I have trouble with my re-verification?</h2>
+<p>Because of the short time that re-verification is open, you <b>will not be able to correct a failed verification</b>. If you think there was an error in the review, please contact us at <a href="stuff">support@edx.org</a>.</p>
\ No newline at end of file
diff --git a/lms/templates/verify_student/prompt_midcourse_reverify.html b/lms/templates/verify_student/prompt_midcourse_reverify.html
new file mode 100644
index 00000000000..b1600366439
--- /dev/null
+++ b/lms/templates/verify_student/prompt_midcourse_reverify.html
@@ -0,0 +1,6 @@
+<%! from django.utils.translation import ugettext as _ %>
+<!--TODO replace this with something a clever deisgn person approves of-->
+<h2>${_("You need to re-verify to continue")}</h2>
+<p class='activation-message'>
+	${_("To continue in the verified track in {course}, you need to re-verify your identity by {date}.  Go to URL.").format(email)}
+</p>
-- 
GitLab