Commit bf210398 by Julia Hansbrough

Tests and cleanup

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