re-verification dashboard styles
This commit is contained in:
committed by
Julia Hansbrough
parent
85030467ed
commit
6c7d715e0e
@@ -10,7 +10,6 @@ import string # pylint: disable=W0402
|
||||
import urllib
|
||||
import uuid
|
||||
import time
|
||||
import datetime
|
||||
from pytz import UTC
|
||||
|
||||
from django.conf import settings
|
||||
@@ -47,7 +46,7 @@ from student.models import (
|
||||
)
|
||||
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 xmodule.course_module import CourseDescriptor
|
||||
@@ -394,21 +393,24 @@ def dashboard(request):
|
||||
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):
|
||||
|
||||
# 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))
|
||||
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)
|
||||
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)
|
||||
)
|
||||
)
|
||||
)
|
||||
prompt_midcourse_reverify = True
|
||||
|
||||
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
|
||||
if _enrollment.refundable())
|
||||
@@ -430,7 +432,6 @@ 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,
|
||||
|
||||
@@ -176,21 +176,16 @@ class XQueueCertInterface(object):
|
||||
is_whitelisted = self.whitelist.filter(
|
||||
user=student, course_id=course_id, whitelist=True).exists()
|
||||
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]
|
||||
course_num = course_id.split('/')[1]
|
||||
cert_mode = enrollment_mode
|
||||
if (
|
||||
(enrollment_mode == GeneratedCertificate.MODES.verified) and
|
||||
SoftwareSecurePhotoVerification.user_is_verified(student) and
|
||||
SSPMidcourseReverification.user_is_reverified_for_all(course_id, student)
|
||||
):
|
||||
if (mode_is_verified and user_is_verified and user_is_reverified):
|
||||
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)) and not
|
||||
(SSPMidcourseReverification.user_is_reverified_for_all(course_id, student))
|
||||
):
|
||||
elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
|
||||
template_pdf = "certificate-template-{0}-{1}.pdf".format(
|
||||
org, course_num)
|
||||
cert_mode = GeneratedCertificate.MODES.honor
|
||||
|
||||
@@ -3,4 +3,4 @@ from verify_student.models import SoftwareSecurePhotoVerification
|
||||
from verify_student.models import MidcourseReverificationWindow
|
||||
|
||||
admin.site.register(SoftwareSecurePhotoVerification)
|
||||
admin.site.register(MidcourseReverificationWindow)
|
||||
admin.site.register(MidcourseReverificationWindow)
|
||||
|
||||
@@ -17,11 +17,19 @@ class Migration(SchemaMigration):
|
||||
))
|
||||
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):
|
||||
# Deleting model 'MidcourseReverificationWindow'
|
||||
db.delete_table('verify_student_midcoursereverificationwindow')
|
||||
|
||||
# Deleting field 'SoftwareSecurePhotoVerification.window'
|
||||
db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
@@ -77,14 +85,15 @@ class Migration(SchemaMigration):
|
||||
'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'}),
|
||||
'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_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']"})
|
||||
'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'})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# -*- 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']
|
||||
@@ -23,7 +23,7 @@ import pytz
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
@@ -38,28 +38,33 @@ 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)
|
||||
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?)
|
||||
There can be many MidcourseReverificationWindows per course, but they cannot have
|
||||
overlapping time ranges. This is enforced by this class's clean() method.
|
||||
"""
|
||||
# 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)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Gives custom validation for the MidcourseReverificationWindow model.
|
||||
Prevents overlapping windows for any particular course.
|
||||
"""
|
||||
query = MidcourseReverificationWindow.objects.filter(course_id=self.course_id)
|
||||
for item in query:
|
||||
if (self.start_date <= item.end_date) and (item.start_date <= self.end_date):
|
||||
raise ValidationError('Reverification windows cannot overlap for a given course.')
|
||||
|
||||
@classmethod
|
||||
def window_open_for_course(cls, course_id):
|
||||
"""
|
||||
@@ -67,16 +72,16 @@ class MidcourseReverificationWindow(models.Model):
|
||||
"""
|
||||
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)
|
||||
cls.objects.get(
|
||||
course_id=course_id,
|
||||
start_date__lte=now,
|
||||
end_date__gte=now,
|
||||
)
|
||||
except(ObjectDoesNotExist):
|
||||
return False
|
||||
|
||||
if (window.start_date <= now <= window.end_date):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_window(cls, course_id, date):
|
||||
@@ -219,8 +224,6 @@ 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']
|
||||
@@ -239,21 +242,22 @@ class PhotoVerification(StatusModel):
|
||||
return allowed_date
|
||||
|
||||
@classmethod
|
||||
def user_is_verified(cls, user, earliest_allowed_date=None):
|
||||
def user_is_verified(cls, user, earliest_allowed_date=None, window=None):
|
||||
"""
|
||||
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.
|
||||
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 cls.objects.filter(
|
||||
user=user,
|
||||
status="approved",
|
||||
created_at__gte=(earliest_allowed_date
|
||||
or cls._earliest_allowed_date())
|
||||
or cls._earliest_allowed_date()),
|
||||
window=window
|
||||
).exists()
|
||||
|
||||
@classmethod
|
||||
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None):
|
||||
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, window=None):
|
||||
"""
|
||||
Return whether the user has a complete verification attempt that is or
|
||||
*might* be good. This means that it's approved, been submitted, or would
|
||||
@@ -261,30 +265,34 @@ class PhotoVerification(StatusModel):
|
||||
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.
|
||||
"""
|
||||
valid_statuses = ['must_retry', 'submitted', 'approved']
|
||||
if window:
|
||||
valid_statuses = ['submitted', 'approved']
|
||||
else:
|
||||
valid_statuses = ['must_retry', 'submitted', 'approved']
|
||||
return cls.objects.filter(
|
||||
user=user,
|
||||
status__in=valid_statuses,
|
||||
created_at__gte=(earliest_allowed_date
|
||||
or cls._earliest_allowed_date())
|
||||
or cls._earliest_allowed_date()),
|
||||
window=window,
|
||||
).exists()
|
||||
|
||||
@classmethod
|
||||
def active_for_user(cls, user):
|
||||
def active_for_user(cls, user, window=None):
|
||||
"""
|
||||
Return the most recent PhotoVerification that is marked ready (i.e. the
|
||||
Return the most recent INITIAL PhotoVerification that is marked ready (i.e. the
|
||||
user has said they're set, but we haven't submitted anything yet).
|
||||
"""
|
||||
# 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.
|
||||
active_attempts = cls.objects.filter(user=user, status='ready').order_by('-created_at')
|
||||
active_attempts = cls.objects.filter(user=user, status='ready', window=window).order_by('-created_at')
|
||||
if active_attempts:
|
||||
return active_attempts[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def user_status(cls, user):
|
||||
def user_status(cls, user, window=None):
|
||||
"""
|
||||
Returns the status of the user based on their past verification attempts
|
||||
|
||||
@@ -297,32 +305,46 @@ class PhotoVerification(StatusModel):
|
||||
status = 'none'
|
||||
error_msg = ''
|
||||
|
||||
if cls.user_is_verified(user):
|
||||
if cls.user_is_verified(user, window=window):
|
||||
status = 'approved'
|
||||
elif cls.user_has_valid_or_pending(user):
|
||||
|
||||
elif cls.user_has_valid_or_pending(user, window=window):
|
||||
# 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')
|
||||
attempts = cls.objects.filter(user=user, window=window).order_by('-updated_at')
|
||||
attempt = attempts[0]
|
||||
except IndexError:
|
||||
return ('none', error_msg)
|
||||
|
||||
# If no verification exists for a *midcourse* reverification, then that just
|
||||
# means the student still needs to reverify. For *original* verifications,
|
||||
# we return 'none'
|
||||
if(window):
|
||||
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 someone is denied their original verification attempt, they can try to reverify.
|
||||
# However, if a midcourse reverification is denied, that denial is permanent.
|
||||
if attempt.status == 'denied':
|
||||
status = 'must_reverify'
|
||||
if window is None:
|
||||
status = 'must_reverify'
|
||||
else:
|
||||
status = 'denied'
|
||||
if attempt.error_msg:
|
||||
error_msg = attempt.parsed_error_msg()
|
||||
|
||||
return (status, error_msg)
|
||||
|
||||
|
||||
def parsed_error_msg(self):
|
||||
"""
|
||||
Sometimes, the error message we've received needs to be parsed into
|
||||
@@ -374,10 +396,6 @@ class PhotoVerification(StatusModel):
|
||||
self.status = "ready"
|
||||
self.save()
|
||||
|
||||
@status_before_must_be("must_retry", "ready", "submitted")
|
||||
def submit(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@status_before_must_be("must_retry", "submitted", "approved", "denied")
|
||||
def approve(self, user_id=None, service=""):
|
||||
"""
|
||||
@@ -508,6 +526,12 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
|
||||
3. The encrypted photos are base64 encoded and stored in an S3 bucket that
|
||||
edx-platform does not have read access to.
|
||||
|
||||
Note: this model handles both *inital* verifications (which you must perform
|
||||
at the time you register for a verified cert), and *midcourse reverifications*.
|
||||
To distinguish between the two, check the value of the property window:
|
||||
intial verifications of a window of None, whereas midcourse reverifications
|
||||
* must always be linked to a specific window*.
|
||||
"""
|
||||
# This is a base64.urlsafe_encode(rsa_encrypt(photo_id_aes_key), ss_pub_key)
|
||||
# So first we generate a random AES-256 key to encrypt our photo ID with.
|
||||
@@ -517,6 +541,43 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
|
||||
IMAGE_LINK_DURATION = 5 * 60 * 60 * 24 # 5 days in seconds
|
||||
|
||||
window = models.ForeignKey(MidcourseReverificationWindow, db_index=True, null=True)
|
||||
|
||||
@classmethod
|
||||
def user_is_reverified_for_all(cls, 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)
|
||||
# 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:
|
||||
# The status of the most recent reverification for each window must be "approved"
|
||||
# for a student to count as completely reverified
|
||||
attempts = cls.objects.filter(user=user, window=window).order_by('-updated_at')
|
||||
attempt = attempts[0]
|
||||
if attempt.status != "approved":
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def original_verification(cls, user):
|
||||
"""
|
||||
Returns the most current SoftwareSecurePhotoVerification object associated with the user.
|
||||
"""
|
||||
query = cls.objects.filter(user=user, window=None).order_by('-updated_at')
|
||||
return query[0]
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_face_image(self, img_data):
|
||||
"""
|
||||
@@ -540,6 +601,19 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
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")
|
||||
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
|
||||
|
||||
self.photo_id_key = self.original_verification(self.user).photo_id_key
|
||||
self.save()
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_photo_id_image(self, img_data):
|
||||
"""
|
||||
@@ -715,6 +789,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
|
||||
return header_txt + "\n\n" + body_txt
|
||||
|
||||
|
||||
def send_request(self):
|
||||
"""
|
||||
Assembles a submission to Software Secure and sends it via HTTPS.
|
||||
@@ -745,210 +820,3 @@ 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
|
||||
|
||||
19
lms/djangoapps/verify_student/tests/factories.py
Normal file
19
lms/djangoapps/verify_student/tests/factories.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,18 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
import json
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from nose.tools import (
|
||||
assert_in, assert_is_none, assert_equals, assert_not_equals, assert_raises,
|
||||
assert_true, assert_false
|
||||
)
|
||||
from mock import MagicMock, patch
|
||||
import pytz
|
||||
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
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
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
|
||||
import verify_student.models
|
||||
|
||||
@@ -208,6 +216,23 @@ class TestPhotoVerification(TestCase):
|
||||
|
||||
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):
|
||||
"""Test that we set our status correctly after a submission."""
|
||||
# Basic case, things go well.
|
||||
@@ -362,3 +387,146 @@ class TestPhotoVerification(TestCase):
|
||||
attempt.error_msg = msg
|
||||
parsed_error_msg = attempt.parsed_error_msg()
|
||||
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
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import CourseEnrollment
|
||||
from course_modes.models import CourseMode
|
||||
from verify_student.views import render_to_response
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
from verify_student.tests.factories import MidcourseReverificationWindowFactory
|
||||
|
||||
|
||||
def mock_render_to_response(*args, **kwargs):
|
||||
@@ -80,6 +82,8 @@ class TestReverifyView(TestCase):
|
||||
def setUp(self):
|
||||
self.user = UserFactory.create(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)
|
||||
def test_reverify_get(self):
|
||||
@@ -110,3 +114,61 @@ class TestReverifyView(TestCase):
|
||||
self.assertIsNotNone(verification_attempt)
|
||||
except ObjectDoesNotExist:
|
||||
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 (
|
||||
get_signed_purchase_params, get_purchase_endpoint
|
||||
)
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification
|
||||
SoftwareSecurePhotoVerification, MidcourseReverificationWindow,
|
||||
)
|
||||
import ssencrypt
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -326,6 +327,7 @@ class ReverifyView(View):
|
||||
}
|
||||
return render_to_response("verify_student/photo_reverification.html", context)
|
||||
|
||||
|
||||
class MidCourseReverifyView(View):
|
||||
"""
|
||||
The mid-course reverification view.
|
||||
@@ -341,10 +343,16 @@ class MidCourseReverifyView(View):
|
||||
"""
|
||||
display this view
|
||||
"""
|
||||
course = course_from_id(course_id)
|
||||
context = {
|
||||
"user_full_name": request.user.profile.name,
|
||||
"error": False,
|
||||
"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)
|
||||
|
||||
@@ -356,7 +364,7 @@ class MidCourseReverifyView(View):
|
||||
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))
|
||||
attempt = SoftwareSecurePhotoVerification(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'))
|
||||
@@ -376,7 +384,12 @@ class MidCourseReverifyView(View):
|
||||
}
|
||||
return render_to_response("verify_student/midcourse_photo_reverification.html", context)
|
||||
|
||||
|
||||
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 = []
|
||||
@@ -397,13 +410,13 @@ def midcourse_reverify_dash(_request):
|
||||
"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):
|
||||
"""
|
||||
@@ -411,6 +424,7 @@ def reverification_submission_confirmation(_request):
|
||||
"""
|
||||
return render_to_response("verify_student/reverification_confirmation.html")
|
||||
|
||||
|
||||
@login_required
|
||||
def midcourse_reverification_confirmation(_request):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// 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();
|
||||
@@ -25,7 +24,7 @@ var submitReverificationPhotos = function() {
|
||||
name: 'face_image',
|
||||
value: $("#face_image")[0].src,
|
||||
}).appendTo("#reverify_form");
|
||||
|
||||
// there is a change here
|
||||
$("#reverify_form").submit();
|
||||
|
||||
}
|
||||
@@ -48,6 +47,7 @@ var submitToPaymentProcessing = function() {
|
||||
"course_id" : course_id,
|
||||
"contribution": contribution,
|
||||
"face_image" : $("#face_image")[0].src,
|
||||
// there is a change here
|
||||
},
|
||||
function(data) {
|
||||
for (prop in data) {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
// base - utilities
|
||||
@import 'base/reset';
|
||||
@import 'base/mixins';
|
||||
@import 'base/variables';
|
||||
@import 'base/mixins';
|
||||
|
||||
## THEMING
|
||||
## -------
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
// base - utilities
|
||||
@import 'base/reset';
|
||||
@import 'base/mixins';
|
||||
@import 'base/variables';
|
||||
@import 'base/mixins';
|
||||
|
||||
## THEMING
|
||||
## -------
|
||||
@@ -41,6 +41,7 @@
|
||||
// base - elements
|
||||
@import 'elements/typography';
|
||||
@import 'elements/controls';
|
||||
@import 'elements/system-feedback';
|
||||
|
||||
// base - specific views
|
||||
@import 'views/verification';
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
// base - utilities
|
||||
@import 'base/reset';
|
||||
@import 'base/mixins';
|
||||
@import 'base/variables';
|
||||
@import 'base/mixins';
|
||||
|
||||
## THEMING
|
||||
## -------
|
||||
|
||||
@@ -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
|
||||
%ui-fake-link {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -308,3 +308,8 @@ $video-thumb-url: '../images/courses/video-thumb.jpg';
|
||||
$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
|
||||
|
||||
// SPLINT: colors
|
||||
|
||||
$msg-bg: $action-primary-bg;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
@import 'base/reset';
|
||||
@import 'base/font_face';
|
||||
@import 'base/mixins';
|
||||
@import 'base/variables';
|
||||
@import 'base/mixins';
|
||||
|
||||
## THEMING
|
||||
## -------
|
||||
|
||||
74
lms/static/sass/elements/_system-feedback.scss
Normal file
74
lms/static/sass/elements/_system-feedback.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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
|
||||
@@ -1,87 +1,6 @@
|
||||
// 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
|
||||
%btn-verify-primary {
|
||||
@extend %btn-primary-green;
|
||||
@@ -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
|
||||
.is-expandable {
|
||||
|
||||
@@ -153,7 +52,8 @@
|
||||
// ====================
|
||||
|
||||
// VIEW: all verification steps
|
||||
.verification-process {
|
||||
.verification-process,
|
||||
.midcourse-reverification-process {
|
||||
|
||||
// reset: box-sizing (making things so right its scary)
|
||||
* {
|
||||
@@ -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 @@
|
||||
</script>
|
||||
</%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 -->
|
||||
%if prompt_midcourse_reverify:
|
||||
<section class="dashboard-banner">
|
||||
<!-- TODO later will need to make this ping for all courses on the dash -->
|
||||
%if reverify_course_data:
|
||||
<section class="dashboard-banner">
|
||||
<div class="wrapper-msg">
|
||||
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
|
||||
</section>
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
% endif
|
||||
|
||||
<section class="container dashboard" id="dashboard-main" aria-hidden="false">
|
||||
|
||||
%if message:
|
||||
<section class="dashboard-banner">
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
<%! 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>
|
||||
% if reverify_course_data:
|
||||
<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:
|
||||
<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)}
|
||||
</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
|
||||
%endif
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
<li class="help-item help-item-whyreverify">
|
||||
<h3 class="title">${_("Why Do I Need to Re-Verify?")}</h3>
|
||||
<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(
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<h2 class="title">
|
||||
%if upgrade:
|
||||
<span class="sts-label">${_("You are upgrading your registration for")}</span>
|
||||
%elif reverify:
|
||||
<span class="sts-label">${_("You are re-verifying for")}</span>
|
||||
%else:
|
||||
<span class="sts-label">${_("You are registering for")}</span>
|
||||
%endif
|
||||
@@ -19,6 +21,8 @@
|
||||
<span class="sts-track-value">
|
||||
%if upgrade:
|
||||
<span class="context">${_("Upgrading to:")}</span> ${_("ID Verified")}
|
||||
%elif reverify:
|
||||
<span class="context">${_("Re-verifying for:")}</span> ${_("ID Verified")}
|
||||
%else:
|
||||
<span class="context">${_("Registering as: ")}</span> ${_("ID Verified")}
|
||||
%endif
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<%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="bodyclass">midcourse-reverification-process is-not-verified step-photos register</%block>
|
||||
<%block name="title"><title>${_("Re-Verify | edX")}</title></%block>
|
||||
|
||||
<%block name="js_extra">
|
||||
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
|
||||
@@ -56,20 +56,13 @@
|
||||
<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">
|
||||
<section class="wrapper">
|
||||
|
||||
<%include file="_verification_header.html" args="course_name=course_name" />
|
||||
|
||||
<div id="wrapper-facephoto" class="wrapper-view block-photo">
|
||||
<div class="facephoto view">
|
||||
<h3 class="title">${_("Re-Take Your Photo")}</h3>
|
||||
@@ -78,6 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wrapper-task">
|
||||
|
||||
<div id="facecam" class="task cam">
|
||||
<div class="placeholder-cam" id="face_capture_div">
|
||||
|
||||
@@ -144,22 +138,22 @@
|
||||
</div>
|
||||
|
||||
|
||||
<li class="review-task review-task-name">
|
||||
<h4 class="title">${_("Check Your Name")}</h4>
|
||||
<div class="review-task review-task-name">
|
||||
|
||||
<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>
|
||||
<h4 class="title">${_("Check Your Name")}</h4>
|
||||
|
||||
<ul class="list-actions">
|
||||
<li class="action action-editname">
|
||||
<a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
</ol>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- TODO janky -->
|
||||
<img id="face_image" src="" style="visibility:hidden;"/>
|
||||
|
||||
@@ -182,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 & re-verify" name="payment">
|
||||
<input class="action-primary disabled" type="button" id="reverify_button" value="Submit photos & re-verify" name="payment">
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -1,28 +1,81 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! 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:
|
||||
<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
|
||||
<div class="copy">
|
||||
<p>You currently need to re-verify for the following course:</p>
|
||||
|
||||
<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>
|
||||
<table class="reverification-table">
|
||||
<tr>
|
||||
<th>Course and Reverification window</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
|
||||
<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>
|
||||
% for course_id, course_name, date, status in reverify_course_data:
|
||||
<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>
|
||||
<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 class="faq-item">
|
||||
<h3 class="title faq-question">Why do I need to re-verify?</h3>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user