Commit 6c7d715e by Frances Botsford Committed by Julia Hansbrough

re-verification dashboard styles

parent 85030467
...@@ -10,7 +10,6 @@ import string # pylint: disable=W0402 ...@@ -10,7 +10,6 @@ import string # pylint: disable=W0402
import urllib import urllib
import uuid import uuid
import time import time
import datetime
from pytz import UTC from pytz import UTC
from django.conf import settings from django.conf import settings
...@@ -47,7 +46,7 @@ from student.models import ( ...@@ -47,7 +46,7 @@ from student.models import (
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
from certificates.models import CertificateStatuses, certificate_status_for_student from certificates.models import CertificateStatuses, certificate_status_for_student
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
...@@ -394,21 +393,24 @@ def dashboard(request): ...@@ -394,21 +393,24 @@ def dashboard(request):
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user) 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... # TODO: Factor this out into a function; I'm pretty sure there's code duplication floating around...
prompt_midcourse_reverify = False
reverify_course_data = [] reverify_course_data = []
for (course, enrollment) in course_enrollment_pairs: 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):
# 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)) window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
status_for_window = SSPMidcourseReverification.get_status_for_window(user, window) if (enrollment.mode == "verified" and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(user, window=window)):
reverify_course_data.append( window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
( status_for_window = SoftwareSecurePhotoVerification.user_status(user, window=window)
course.id, reverify_course_data.append(
course.display_name, (
window.end_date, course.id,
"must_reverify" # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure) 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 show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if _enrollment.refundable()) if _enrollment.refundable())
...@@ -430,7 +432,6 @@ def dashboard(request): ...@@ -430,7 +432,6 @@ def dashboard(request):
'all_course_modes': course_modes, 'all_course_modes': course_modes,
'cert_statuses': cert_statuses, 'cert_statuses': cert_statuses,
'show_email_settings_for': show_email_settings_for, 'show_email_settings_for': show_email_settings_for,
'prompt_midcourse_reverify': prompt_midcourse_reverify,
'reverify_course_data': reverify_course_data, 'reverify_course_data': reverify_course_data,
'verification_status': verification_status, 'verification_status': verification_status,
'verification_msg': verification_msg, 'verification_msg': verification_msg,
......
...@@ -176,21 +176,16 @@ class XQueueCertInterface(object): ...@@ -176,21 +176,16 @@ class XQueueCertInterface(object):
is_whitelisted = self.whitelist.filter( is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists() user=student, course_id=course_id, whitelist=True).exists()
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id) enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
org = course_id.split('/')[0] org = course_id.split('/')[0]
course_num = course_id.split('/')[1] course_num = course_id.split('/')[1]
cert_mode = enrollment_mode cert_mode = enrollment_mode
if ( if (mode_is_verified and user_is_verified and user_is_reverified):
(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( template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
org, course_num) org, course_num)
elif ( elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
(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( template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num) org, course_num)
cert_mode = GeneratedCertificate.MODES.honor cert_mode = GeneratedCertificate.MODES.honor
......
...@@ -3,4 +3,4 @@ from verify_student.models import SoftwareSecurePhotoVerification ...@@ -3,4 +3,4 @@ from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.models import MidcourseReverificationWindow from verify_student.models import MidcourseReverificationWindow
admin.site.register(SoftwareSecurePhotoVerification) admin.site.register(SoftwareSecurePhotoVerification)
admin.site.register(MidcourseReverificationWindow) admin.site.register(MidcourseReverificationWindow)
\ No newline at end of file
...@@ -17,11 +17,19 @@ class Migration(SchemaMigration): ...@@ -17,11 +17,19 @@ class Migration(SchemaMigration):
)) ))
db.send_create_signal('verify_student', ['MidcourseReverificationWindow']) db.send_create_signal('verify_student', ['MidcourseReverificationWindow'])
# Adding field 'SoftwareSecurePhotoVerification.window'
db.add_column('verify_student_softwaresecurephotoverification', 'window',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.MidcourseReverificationWindow'], null=True),
keep_default=False)
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'MidcourseReverificationWindow' # Deleting model 'MidcourseReverificationWindow'
db.delete_table('verify_student_midcoursereverificationwindow') db.delete_table('verify_student_midcoursereverificationwindow')
# Deleting field 'SoftwareSecurePhotoVerification.window'
db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
models = { models = {
'auth.group': { 'auth.group': {
...@@ -77,14 +85,15 @@ class Migration(SchemaMigration): ...@@ -77,14 +85,15 @@ class Migration(SchemaMigration):
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': '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_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}), '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'}), 'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1d47320>'", 'max_length': '255', 'db_index': 'True'}),
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': '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']"}), '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': ('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'"}), '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'}), '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'}), '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']"}) 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.MidcourseReverificationWindow']", 'null': 'True'})
} }
} }
......
# -*- 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
"""
verify_student factories
"""
from verify_student.models import MidcourseReverificationWindow
from factory.django import DjangoModelFactory
import pytz
from datetime import timedelta, datetime
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232
class MidcourseReverificationWindowFactory(DjangoModelFactory):
""" Creates a generic MidcourseReverificationWindow. """
FACTORY_FOR = MidcourseReverificationWindow
course_id = u'MITx/999/Robot_Super_Course'
# By default this factory creates a window that is currently open
start_date = datetime.now(pytz.UTC) - timedelta(days=100)
end_date = datetime.now(pytz.UTC) + timedelta(days=100)
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import timedelta from datetime import timedelta, datetime
import json import json
from xmodule.modulestore.tests.factories import CourseFactory
from nose.tools import ( from nose.tools import (
assert_in, assert_is_none, assert_equals, assert_not_equals, assert_raises, assert_in, assert_is_none, assert_equals, assert_not_equals, assert_raises,
assert_true, assert_false assert_true, assert_false
) )
from mock import MagicMock, patch from mock import MagicMock, patch
import pytz
from django.test import TestCase from django.test import TestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from django.test.utils import override_settings
from django.conf import settings from django.conf import settings
import requests import requests
import requests.exceptions import requests.exceptions
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from verify_student.models import SoftwareSecurePhotoVerification, VerificationException from verify_student.models import (
SoftwareSecurePhotoVerification, VerificationException,
MidcourseReverificationWindow,
)
from verify_student.tests.factories import MidcourseReverificationWindowFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
import verify_student.models import verify_student.models
...@@ -208,6 +216,23 @@ class TestPhotoVerification(TestCase): ...@@ -208,6 +216,23 @@ class TestPhotoVerification(TestCase):
return attempt return attempt
def test_fetch_photo_id_image(self):
user = UserFactory.create()
orig_attempt = SoftwareSecurePhotoVerification(user=user, window=None)
orig_attempt.save()
old_key = orig_attempt.photo_id_key
window = MidcourseReverificationWindowFactory(
course_id="ponies",
start_date=datetime.now(pytz.utc) - timedelta(days=5),
end_date=datetime.now(pytz.utc) + timedelta(days=5)
)
new_attempt = SoftwareSecurePhotoVerification(user=user, window=window)
new_attempt.save()
new_attempt.fetch_photo_id_image()
assert_equals(new_attempt.photo_id_key, old_key)
def test_submissions(self): def test_submissions(self):
"""Test that we set our status correctly after a submission.""" """Test that we set our status correctly after a submission."""
# Basic case, things go well. # Basic case, things go well.
...@@ -362,3 +387,146 @@ class TestPhotoVerification(TestCase): ...@@ -362,3 +387,146 @@ class TestPhotoVerification(TestCase):
attempt.error_msg = msg attempt.error_msg = msg
parsed_error_msg = attempt.parsed_error_msg() parsed_error_msg = attempt.parsed_error_msg()
self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.") self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.")
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidcourseReverificationWindow(TestCase):
""" Tests for MidcourseReverificationWindow objects """
def setUp(self):
self.course_id = "MITx/999/Robot_Super_Course"
CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
def test_window_open_for_course(self):
# Should return False if no windows exist for a course
self.assertFalse(MidcourseReverificationWindow.window_open_for_course(self.course_id))
# Should return False if a window exists, but it's not in the current timeframe
MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=10),
end_date=datetime.now(pytz.utc) - timedelta(days=5)
)
self.assertFalse(MidcourseReverificationWindow.window_open_for_course(self.course_id))
# Should return True if a non-expired window exists
MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
self.assertTrue(MidcourseReverificationWindow.window_open_for_course(self.course_id))
def test_get_window(self):
# if no window exists, returns None
self.assertIsNone(MidcourseReverificationWindow.get_window(self.course_id, datetime.now(pytz.utc)))
# we should get the expected window otherwise
window_valid = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
self.assertEquals(
window_valid,
MidcourseReverificationWindow.get_window(self.course_id, datetime.now(pytz.utc))
)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@patch('verify_student.models.S3Connection', new=MockS3Connection)
@patch('verify_student.models.Key', new=MockKey)
@patch('verify_student.models.requests.post', new=mock_software_secure_post)
class TestMidcourseReverification(TestCase):
def setUp(self):
self.course_id = "MITx/999/Robot_Super_Course"
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
self.user = UserFactory.create()
def test_user_is_reverified_for_all(self):
# if there are no windows for a course, this should return True
self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
# first, make three windows
window1 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
window2 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=10),
end_date=datetime.now(pytz.UTC) - timedelta(days=8),
)
window3 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=5),
end_date=datetime.now(pytz.UTC) - timedelta(days=3),
)
# make two SSPMidcourseReverifications for those windows
attempt1 = SoftwareSecurePhotoVerification(
status="approved",
user=self.user,
window=window1
)
attempt1.save()
attempt2 = SoftwareSecurePhotoVerification(
status="approved",
user=self.user,
window=window2
)
attempt2.save()
# should return False because only 2 of 3 windows have verifications
self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
attempt3 = SoftwareSecurePhotoVerification(
status="must_retry",
user=self.user,
window=window3
)
attempt3.save()
# should return False because the last verification exists BUT is not approved
self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
attempt3.status = "approved"
attempt3.save()
# should now return True because all windows have approved verifications
self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
def test_original_verification(self):
orig_attempt = SoftwareSecurePhotoVerification(user=self.user)
orig_attempt.save()
window = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
midcourse_attempt = SoftwareSecurePhotoVerification(user=self.user, window=window)
self.assertEquals(midcourse_attempt.original_verification(user=self.user), orig_attempt)
def test_user_has_valid_or_pending(self):
window = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
attempt = SoftwareSecurePhotoVerification(status="must_retry", user=self.user, window=window)
attempt.save()
assert_false(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user=self.user, window=window))
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
...@@ -21,9 +21,11 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -21,9 +21,11 @@ from django.core.exceptions import ObjectDoesNotExist
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from student.models import CourseEnrollment
from course_modes.models import CourseMode from course_modes.models import CourseMode
from verify_student.views import render_to_response from verify_student.views import render_to_response
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.tests.factories import MidcourseReverificationWindowFactory
def mock_render_to_response(*args, **kwargs): def mock_render_to_response(*args, **kwargs):
...@@ -80,6 +82,8 @@ class TestReverifyView(TestCase): ...@@ -80,6 +82,8 @@ class TestReverifyView(TestCase):
def setUp(self): def setUp(self):
self.user = UserFactory.create(username="rusty", password="test") self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test") self.client.login(username="rusty", password="test")
self.course_id = "MITx/999/Robot_Super_Course"
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
@patch('verify_student.views.render_to_response', render_mock) @patch('verify_student.views.render_to_response', render_mock)
def test_reverify_get(self): def test_reverify_get(self):
...@@ -110,3 +114,61 @@ class TestReverifyView(TestCase): ...@@ -110,3 +114,61 @@ class TestReverifyView(TestCase):
self.assertIsNotNone(verification_attempt) self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist: except ObjectDoesNotExist:
self.fail('No verification object generated') self.fail('No verification object generated')
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):
def setUp(self):
self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test")
self.course_id = 'Robot/999/Test_Course'
CourseFactory.create(org='Robot', number='999', display_name='Test Course')
@patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_get(self):
url = reverse('verify_student_midcourse_reverify',
kwargs={"course_id": self.course_id})
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
((_template, context), _kwargs) = render_mock.call_args
self.assertFalse(context['error'])
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_midcourse_reverify_post_success(self):
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)
self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist:
self.fail('No verification object generated')
# TODO make this test more detailed
@patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_dash(self):
url = reverse('verify_student_midcourse_reverify_dash')
response = self.client.get(url)
# not enrolled in any courses
self.assertEquals(response.status_code, 200)
enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id)
enrollment.update_enrollment(mode="verified", is_active=True)
MidcourseReverificationWindowFactory(course_id=self.course_id)
response = self.client.get(url)
# enrolled in a verified course, and the window is open
self.assertEquals(response.status_code, 200)
...@@ -30,9 +30,10 @@ from shoppingcart.processors.CyberSource import ( ...@@ -30,9 +30,10 @@ from shoppingcart.processors.CyberSource import (
get_signed_purchase_params, get_purchase_endpoint get_signed_purchase_params, get_purchase_endpoint
) )
from verify_student.models import ( from verify_student.models import (
SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification SoftwareSecurePhotoVerification, MidcourseReverificationWindow,
) )
import ssencrypt import ssencrypt
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -326,6 +327,7 @@ class ReverifyView(View): ...@@ -326,6 +327,7 @@ class ReverifyView(View):
} }
return render_to_response("verify_student/photo_reverification.html", context) return render_to_response("verify_student/photo_reverification.html", context)
class MidCourseReverifyView(View): class MidCourseReverifyView(View):
""" """
The mid-course reverification view. The mid-course reverification view.
...@@ -341,10 +343,16 @@ class MidCourseReverifyView(View): ...@@ -341,10 +343,16 @@ class MidCourseReverifyView(View):
""" """
display this view display this view
""" """
course = course_from_id(course_id)
context = { context = {
"user_full_name": request.user.profile.name, "user_full_name": request.user.profile.name,
"error": False, "error": False,
"course_id": course_id, "course_id": course_id,
"course_name": course.display_name_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"reverify": True,
} }
return render_to_response("verify_student/midcourse_photo_reverification.html", context) return render_to_response("verify_student/midcourse_photo_reverification.html", context)
...@@ -356,7 +364,7 @@ class MidCourseReverifyView(View): ...@@ -356,7 +364,7 @@ class MidCourseReverifyView(View):
try: try:
# TODO look at this more carefully! #1 testing candidate # TODO look at this more carefully! #1 testing candidate
now = datetime.datetime.now(UTC) now = datetime.datetime.now(UTC)
attempt = SSPMidcourseReverification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now)) attempt = SoftwareSecurePhotoVerification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now))
b64_face_image = request.POST['face_image'].split(",")[1] b64_face_image = request.POST['face_image'].split(",")[1]
attempt.upload_face_image(b64_face_image.decode('base64')) attempt.upload_face_image(b64_face_image.decode('base64'))
...@@ -376,7 +384,12 @@ class MidCourseReverifyView(View): ...@@ -376,7 +384,12 @@ class MidCourseReverifyView(View):
} }
return render_to_response("verify_student/midcourse_photo_reverification.html", context) return render_to_response("verify_student/midcourse_photo_reverification.html", context)
def midcourse_reverify_dash(_request): 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 # TODO same comment as in student/views.py: need to factor out this functionality
user = _request.user user = _request.user
course_enrollment_pairs = [] course_enrollment_pairs = []
...@@ -397,13 +410,13 @@ def midcourse_reverify_dash(_request): ...@@ -397,13 +410,13 @@ def midcourse_reverify_dash(_request):
"must_reverify" "must_reverify"
) )
) )
prompt_midcourse_reverify = True
context = { context = {
"user_full_name": _request.user.profile.name, "user_full_name": _request.user.profile.name,
"reverify_course_data": reverify_course_data, "reverify_course_data": reverify_course_data,
} }
return render_to_response("verify_student/midcourse_reverify_dash.html", context) return render_to_response("verify_student/midcourse_reverify_dash.html", context)
@login_required @login_required
def reverification_submission_confirmation(_request): def reverification_submission_confirmation(_request):
""" """
...@@ -411,6 +424,7 @@ def reverification_submission_confirmation(_request): ...@@ -411,6 +424,7 @@ def reverification_submission_confirmation(_request):
""" """
return render_to_response("verify_student/reverification_confirmation.html") return render_to_response("verify_student/reverification_confirmation.html")
@login_required @login_required
def midcourse_reverification_confirmation(_request): def midcourse_reverification_confirmation(_request):
""" """
......
// TODO diff this against photocapture.js, see if I actually needed a whole honking new file
var onVideoFail = function(e) { var onVideoFail = function(e) {
if(e == 'NO_DEVICES_FOUND') { if(e == 'NO_DEVICES_FOUND') {
$('#no-webcam').show(); $('#no-webcam').show();
...@@ -25,7 +24,7 @@ var submitReverificationPhotos = function() { ...@@ -25,7 +24,7 @@ var submitReverificationPhotos = function() {
name: 'face_image', name: 'face_image',
value: $("#face_image")[0].src, value: $("#face_image")[0].src,
}).appendTo("#reverify_form"); }).appendTo("#reverify_form");
// there is a change here
$("#reverify_form").submit(); $("#reverify_form").submit();
} }
...@@ -48,6 +47,7 @@ var submitToPaymentProcessing = function() { ...@@ -48,6 +47,7 @@ var submitToPaymentProcessing = function() {
"course_id" : course_id, "course_id" : course_id,
"contribution": contribution, "contribution": contribution,
"face_image" : $("#face_image")[0].src, "face_image" : $("#face_image")[0].src,
// there is a change here
}, },
function(data) { function(data) {
for (prop in data) { for (prop in data) {
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
// base - utilities // base - utilities
@import 'base/reset'; @import 'base/reset';
@import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins';
## THEMING ## THEMING
## ------- ## -------
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
// base - utilities // base - utilities
@import 'base/reset'; @import 'base/reset';
@import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins';
## THEMING ## THEMING
## ------- ## -------
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
// base - elements // base - elements
@import 'elements/typography'; @import 'elements/typography';
@import 'elements/controls'; @import 'elements/controls';
@import 'elements/system-feedback';
// base - specific views // base - specific views
@import 'views/verification'; @import 'views/verification';
......
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
// base - utilities // base - utilities
@import 'base/reset'; @import 'base/reset';
@import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins';
## THEMING ## THEMING
## ------- ## -------
......
...@@ -54,6 +54,30 @@ ...@@ -54,6 +54,30 @@
// ==================== // ====================
// extends - UI - used for page/view-level wrappers (for centering/grids)
%ui-wrapper {
@include clearfix();
@include box-sizing(border-box);
width: 100%;
}
// extends - UI - window
%ui-window {
@include clearfix();
border-radius: 3px;
box-shadow: 0 1px 2px 1px $shadow-l1;
margin-bottom: $baseline;
border: 1px solid $light-gray;
background: $white;
}
// extends - UI archetypes - well
%ui-well {
box-shadow: inset 0 1px 2px 1px $shadow-l1;
padding: ($baseline*0.75) $baseline;
}
// extends - UI - visual link // extends - UI - visual link
%ui-fake-link { %ui-fake-link {
cursor: pointer; cursor: pointer;
......
...@@ -308,3 +308,8 @@ $video-thumb-url: '../images/courses/video-thumb.jpg'; ...@@ -308,3 +308,8 @@ $video-thumb-url: '../images/courses/video-thumb.jpg';
$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif; $f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif;
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif; $f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace; $f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// SPLINT: colors
$msg-bg: $action-primary-bg;
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
@import 'base/reset'; @import 'base/reset';
@import 'base/font_face'; @import 'base/font_face';
@import 'base/mixins';
@import 'base/variables'; @import 'base/variables';
@import 'base/mixins';
## THEMING ## THEMING
## ------- ## -------
......
// lms - elements - system feedback
// ====================
// messages
// UI : message
.wrapper-msg {
box-shadow: 0 0 5px $action-primary-shadow inset;
margin-bottom: ($baseline*1.5);
padding: $baseline ($baseline*1.5);
background: $action-primary-bg;
.msg {
@include clearfix();
max-width: grid-width(12);
min-width: 760px;
width: flex-grid(12);
margin: 0 auto;
}
.msg-content,
.msg-icon {
display: inline-block;
vertical-align: middle;
}
.msg-content {
width: flex-grid(10,12);
.title {
@extend %t-title5;
@extend %t-weight4;
margin-bottom: ($baseline/4);
color: $white;
text-transform: none;
letter-spacing: 0;
}
.copy {
@extend %t-copy-sub1;
color: $white;
p { // nasty reset
@extend %t-copy-sub1;
color: $white;
}
}
}
.has-actions {
.msg-content {
width: flex-grid(10,12);
}
.nav-actions {
width: flex-grid(2,12);
display: inline-block;
vertical-align: middle;
.action-primary {
@extend %btn-primary-green;
}
}
}
}
// prompts
// notifications
// alerts
// lms - views - verification flow // lms - views - verification flow
// ==================== // ====================
// MISC: extends - type
// application: canned headings
%hd-lv1 {
@extend %t-title1;
@extend %t-weight1;
color: $m-gray-d4;
margin: 0 0 ($baseline*2) 0;
}
%hd-lv2 {
@extend %t-title4;
@extend %t-weight1;
margin: 0 0 ($baseline*0.75) 0;
border-bottom: 1px solid $m-gray-l4;
padding-bottom: ($baseline/2);
color: $m-gray-d4;
}
%hd-lv3 {
@extend %t-title6;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
%hd-lv4 {
@extend %t-title6;
@extend %t-weight2;
margin: 0 0 $baseline 0;
color: $m-gray-d4;
}
%hd-lv5 {
@extend %t-title7;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
// application: canned copy
%copy-base {
@extend %t-copy-base;
color: $m-gray-d2;
}
%copy-lead1 {
@extend %t-copy-lead2;
color: $m-gray;
}
%copy-detail {
@extend %t-copy-sub1;
@extend %t-weight3;
color: $m-gray-d1;
}
%copy-metadata {
@extend %t-copy-sub2;
color: $m-gray-d1;
%copy-metadata-value {
@extend %t-weight2;
}
%copy-metadata-value {
@extend %t-weight4;
}
}
// application: canned links
%copy-link {
border-bottom: 1px dotted transparent;
&:hover, &:active {
border-color: $link-color-d1;
}
}
// ====================
// MISC: extends - button // MISC: extends - button
%btn-verify-primary { %btn-verify-primary {
@extend %btn-primary-green; @extend %btn-primary-green;
...@@ -89,26 +8,6 @@ ...@@ -89,26 +8,6 @@
// ==================== // ====================
// MISC: extends - UI - window
%ui-window {
@include clearfix();
border-radius: ($baseline/10);
box-shadow: 0 1px 2px 1px $shadow-l1;
margin-bottom: $baseline;
border: 1px solid $m-gray-l3;
background: $white;
}
// ====================
// MISC: extends - UI - well
%ui-well {
box-shadow: inset 0 1px 2px 1px $shadow-l1;
padding: ($baseline*0.75) $baseline;
}
// ====================
// MISC: expandable UI // MISC: expandable UI
.is-expandable { .is-expandable {
...@@ -153,7 +52,8 @@ ...@@ -153,7 +52,8 @@
// ==================== // ====================
// VIEW: all verification steps // VIEW: all verification steps
.verification-process { .verification-process,
.midcourse-reverification-process {
// reset: box-sizing (making things so right its scary) // reset: box-sizing (making things so right its scary)
* { * {
...@@ -1894,6 +1794,335 @@ ...@@ -1894,6 +1794,335 @@
} }
} }
} }
// VIEW: midcourse re-verification
&.midcourse-reverification-process {
// step-dash
.action-reverify {
@extend %btn-verify-primary;
padding: ($baseline/2) ($baseline*0.75);
}
.reverification-table {
width: 100%;
th {
display: none;
}
th,
td {
padding: ($baseline/2) 0;
text-align: left;
border-bottom: 1px solid $light-gray;
}
.course-name {
@extend %t-title5;
display: block;
font-weight: bold;
}
.deadline {
@extend %copy-detail;
display: block;
}
}
.wrapper-reverification-help {
margin-top: $baseline*2;
.faq-item {
display: inline-block;
vertical-align: top;
width: flex-grid(4,12);
padding-right: $baseline;
&:last-child {
padding-right: 0;
}
.faq-answer {
@extend %t-copy-sub1;
}
}
}
// step-photos
.wrapper-task {
@include clearfix();
width: flex-grid(12,12);
margin: $baseline 0;
.wrapper-help {
float: right;
width: flex-grid(6,12);
padding: 0 $baseline;
.help {
margin-bottom: ($baseline*1.5);
&:last-child {
margin-bottom: 0;
}
.title {
@extend %hd-lv3;
}
.copy {
@extend %copy-detail;
}
.example {
color: $m-gray-l2;
}
// help - general list
.list-help {
margin-top: ($baseline/2);
color: $black;
.help-item {
margin-bottom: ($baseline/4);
border-bottom: 1px solid $m-gray-l4;
padding-bottom: ($baseline/4);
&:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
}
.help-item-emphasis {
@extend %t-weight4;
}
}
// help - faq
.list-faq {
margin-bottom: $baseline;
}
}
}
.task {
@extend %ui-window;
float: left;
width: flex-grid(6,12);
margin-right: flex-gutter();
}
.controls {
padding: ($baseline*0.75) $baseline;
background: $m-gray-l4;
.list-controls {
position: relative;
}
.control {
position: absolute;
.action {
@extend %btn-primary-blue;
padding: ($baseline/2) ($baseline*0.75);
*[class^="icon-"] {
@extend %t-icon4;
padding: ($baseline*.25) ($baseline*.5);
display: block;
}
}
// STATE: hidden
&.is-hidden {
visibility: hidden;
}
// STATE: shown
&.is-shown {
visibility: visible;
}
// STATE: approved
&.approved {
.action {
@extend %btn-verify-primary;
padding: ($baseline/2) ($baseline*0.75);
}
}
}
// control - redo
.control-redo {
position: absolute;
left: ($baseline/2);
}
// control - take/do
.control-do {
left: 45%;
}
// control - approve
.control-approve {
position: absolute;
right: ($baseline/2);
}
}
.msg {
@include clearfix();
margin-top: ($baseline*2);
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.list-actions {
position: relative;
top: -($baseline/2);
float: left;
width: flex-grid(4,12);
text-align: right;
.action-retakephotos a {
@extend %btn-primary-blue;
@include font-size(14);
padding: ($baseline/2) ($baseline*.75);
}
}
}
.msg-followup {
border-top: ($baseline/10) solid $m-gray-t0;
padding-top: $baseline;
}
}
.review-task {
margin-bottom: ($baseline*1.5);
padding: ($baseline*0.75) $baseline;
border-radius: ($baseline/10);
background: $m-gray-l4;
&:last-child {
margin-bottom: 0;
}
> .title {
@extend %hd-lv3;
}
.copy {
@extend %copy-base;
strong {
@extend %t-weight5;
color: $m-gray-d4;
}
}
}
// individual task - name
.review-task-name {
@include clearfix();
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.list-actions {
position: relative;
top: -($baseline);
float: left;
width: flex-grid(4,12);
text-align: right;
.action-editname a {
@extend %btn-primary-blue;
@include font-size(14);
padding: ($baseline/2) ($baseline*.75);
}
}
}
.nav-wizard {
padding: ($baseline*.75) $baseline;
.prompt-verify {
float: left;
width: flex-grid(6,12);
margin: 0 flex-gutter() 0 0;
.title {
@extend %hd-lv4;
margin-bottom: ($baseline/4);
}
.copy {
@extend %t-copy-sub1;
@extend %t-weight3;
}
.list-actions {
margin-top: ($baseline/2);
}
.action-verify label {
@extend %t-copy-sub1;
}
}
.wizard-steps {
margin-top: ($baseline/2);
.wizard-step {
margin-right: flex-gutter();
display: inline-block;
vertical-align: middle;
&:last-child {
margin-right: 0;
}
}
}
}
.modal {
fieldset {
margin-top: $baseline;
}
.close-modal {
@include font-size(24);
color: $m-blue-d3;
&:hover, &:focus {
color: $m-blue-d1;
border: none;
}
}
}
}
} }
// ==================== // ====================
......
...@@ -152,14 +152,17 @@ ...@@ -152,14 +152,17 @@
</script> </script>
</%block> </%block>
<section class="container dashboard" id="dashboard-main" aria-hidden="false">
<!-- TODO later will need to make this ping for all courses on the dash --> <!-- TODO later will need to make this ping for all courses on the dash -->
%if prompt_midcourse_reverify: %if reverify_course_data:
<section class="dashboard-banner"> <section class="dashboard-banner">
<div class="wrapper-msg">
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' /> <%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
</section> </div>
% endif </section>
% endif
<section class="container dashboard" id="dashboard-main" aria-hidden="false">
%if message: %if message:
<section class="dashboard-banner"> <section class="dashboard-banner">
......
...@@ -2,12 +2,24 @@ ...@@ -2,12 +2,24 @@
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<!--TODO replace this with something a clever deisgn person approves of--> <!--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--> <!--TODO replace this with a shiny loopy thing to actually print out all courses-->
% if prompt_midcourse_reverify: % if reverify_course_data:
<h2>${_("You need to re-verify to continue")}</h2> <div class="msg msg-reverify has-actions">
% for course_id, course_name, date, status in reverify_course_data: <div class="msg-content">
<p class='activation-message'> <h2 class="title">${_("You need to re-verify to continue")}</h2>
${_('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)} % for course_id, course_name, date, status in reverify_course_data:
<a href="${reverse('verify_student_midcourse_reverify_dash')}">Click here to re-verify.</a> <div class="copy">
</p> <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)}
</p>
</div>
</div>
<nav class="nav-actions">
<h3 class="sr">Notification Actions</h3>
<ul>
<li class="nav-item"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify_dash')}">Re-verify</a></li>
</ul>
</nav>
</div>
</div>
% endfor % endfor
%endif %endif
...@@ -6,12 +6,7 @@ ...@@ -6,12 +6,7 @@
<li class="help-item help-item-whyreverify"> <li class="help-item help-item-whyreverify">
<h3 class="title">${_("Why Do I Need to Re-Verify?")}</h3> <h3 class="title">${_("Why Do I Need to Re-Verify?")}</h3>
<div class="copy"> <div class="copy">
<p>${_("There was a problem with your original verification. To make sure that your identity is correctly associated with your course progress, we need to retake your photo and a photo of your identification document. If you don't have a valid identification document, contact {link_start}{support_email}{link_end}.").format( <p>${_("At key points in a course, the professor will ask you to re-verify your identity. 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.")}</p>
support_email=settings.DEFAULT_FEEDBACK_EMAIL,
link_start=u'<a href="mailto:{address}?subject={subject_line}">'.format(
address=settings.DEFAULT_FEEDBACK_EMAIL,
subject_line=_('Problem with ID re-verification')),
link_end=u'</a>')}</p>
</div> </div>
</li> </li>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
<h2 class="title"> <h2 class="title">
%if upgrade: %if upgrade:
<span class="sts-label">${_("You are upgrading your registration for")}</span> <span class="sts-label">${_("You are upgrading your registration for")}</span>
%elif reverify:
<span class="sts-label">${_("You are re-verifying for")}</span>
%else: %else:
<span class="sts-label">${_("You are registering for")}</span> <span class="sts-label">${_("You are registering for")}</span>
%endif %endif
...@@ -19,6 +21,8 @@ ...@@ -19,6 +21,8 @@
<span class="sts-track-value"> <span class="sts-track-value">
%if upgrade: %if upgrade:
<span class="context">${_("Upgrading to:")}</span> ${_("ID Verified")} <span class="context">${_("Upgrading to:")}</span> ${_("ID Verified")}
%elif reverify:
<span class="context">${_("Re-verifying for:")}</span> ${_("ID Verified")}
%else: %else:
<span class="context">${_("Registering as: ")}</span> ${_("ID Verified")} <span class="context">${_("Registering as: ")}</span> ${_("ID Verified")}
%endif %endif
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">register verification-process is-not-verified step-photos</%block> <%block name="bodyclass">midcourse-reverification-process is-not-verified step-photos register</%block>
<%block name="title"><title>${_("Re-Verification")}</title></%block> <%block name="title"><title>${_("Re-Verify | edX")}</title></%block>
<%block name="js_extra"> <%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.js')}"></script>
...@@ -56,20 +56,13 @@ ...@@ -56,20 +56,13 @@
<div class="container"> <div class="container">
<section class="wrapper"> <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"> <div class="wrapper-content-main">
<article class="content-main"> <article class="content-main">
<section class="wrapper carousel" data-transition="slide"> <section class="wrapper">
<%include file="_verification_header.html" args="course_name=course_name" />
<div id="wrapper-facephoto" class="wrapper-view block-photo"> <div id="wrapper-facephoto" class="wrapper-view block-photo">
<div class="facephoto view"> <div class="facephoto view">
<h3 class="title">${_("Re-Take Your Photo")}</h3> <h3 class="title">${_("Re-Take Your Photo")}</h3>
...@@ -78,6 +71,7 @@ ...@@ -78,6 +71,7 @@
</div> </div>
<div class="wrapper-task"> <div class="wrapper-task">
<div id="facecam" class="task cam"> <div id="facecam" class="task cam">
<div class="placeholder-cam" id="face_capture_div"> <div class="placeholder-cam" id="face_capture_div">
...@@ -144,22 +138,22 @@ ...@@ -144,22 +138,22 @@
</div> </div>
<li class="review-task review-task-name"> <div class="review-task review-task-name">
<h4 class="title">${_("Check Your Name")}</h4>
<div class="copy"> <h4 class="title">${_("Check Your Name")}</h4>
<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"> <div class="copy">
<li class="action action-editname"> <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>
<a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a> </div>
</li>
</ul> <ul class="list-actions">
<li class="action action-editname">
<a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a>
</li> </li>
</ol> </ul>
</div> </div>
<!-- TODO janky --> <!-- TODO janky -->
<img id="face_image" src="" style="visibility:hidden;"/> <img id="face_image" src="" style="visibility:hidden;"/>
...@@ -182,7 +176,7 @@ ...@@ -182,7 +176,7 @@
<form id="reverify_form" method="post"> <form id="reverify_form" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<input type="hidden" name="course_id" value="${course_id}"> <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"> <input class="action-primary disabled" type="button" id="reverify_button" value="Submit photos &amp; re-verify" name="payment">
</form> </form>
</li> </li>
</ol> </ol>
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%block name="bodyclass">midcourse-reverification-process step-dash register</%block>
<%block name="title">
<title>
${_("Reverification Status | edX")}
</title>
</%block>
<h1>Re-verify</h1> <%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<p>You currently need to re-verify for the following course:</p> <h2 class="title">You are re-verifying your identity</h2>
% for course_id, course_name, date, status in reverify_course_data: <div class="copy">
<p>${course_name}: Re-verify by ${date}. <p>You currently need to re-verify for the following course:</p>
% 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> <table class="reverification-table">
<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> <tr>
<th>Course and Reverification window</th>
<th>Status</th>
</tr>
<h2>What will I need to re-verify?</h2> % for course_id, course_name, date, status in reverify_course_data:
<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> <tr>
<td>
<span class="course-name">${course_name} (HKS211.1x)</span>
<span class="deadline">Re-verify by <strong>${date}</strong></span>
</td>
<td>
% if status == "must_reverify":
<a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': course_id})}">Re-verify for HKS211.1x</a>
% elif status == "completed":
Completed
% elif status == "failed":
Failed
% endif
</td>
</tr>
% endfor
</table>
</div>
<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> <div class="wrapper-reverification-help list-faq">
<h2>What if I have trouble with my re-verification?</h2> <div class="faq-item">
<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> <h3 class="title faq-question">Why do I need to re-verify?</h3>
\ No newline at end of file <div class="copy faq-answer">
<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>
</div>
</div>
<div class="faq-item">
<h3 class="title faq-question">What will I need to re-verify?</h3>
<div class="copy faq-answer">
<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>
</div>
</div>
<div class="faq-item">
<h3 class="title faq-question">What if I have trouble with my re-verification?</h3>
<div class="copy faq-answer">
<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>
</div>
</div>
</div>
</article>
</div> <!-- /wrapper-content-main -->
</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