Skip to content
Snippets Groups Projects
Commit 85030467 authored by Julia Hansbrough's avatar Julia Hansbrough
Browse files

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
parent 65997d0f
No related merge requests found
Showing
with 1219 additions and 11 deletions
......@@ -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,
......
......@@ -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
......
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
# -*- 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
# -*- 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
......@@ -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
......@@ -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"
),
)
......@@ -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")
// 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);
});
......@@ -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}
......
<%! 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
<%! 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>
<%! 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>
<%! 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
<%! 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>
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment