Commit cac83948 by Julia Hansbrough

Merge pull request #2156 from edx/flowerhack/feature/basic-reverify-flow

Follow-On Verification
parents c01ea902 17869c5e
...@@ -454,6 +454,8 @@ INSTALLED_APPS = ( ...@@ -454,6 +454,8 @@ INSTALLED_APPS = (
# Dark-launching languages # Dark-launching languages
'dark_lang', 'dark_lang',
# Student identity reverification
'reverification',
) )
......
"""
Reverification admin
"""
from ratelimitbackend import admin
from reverification.models import MidcourseReverificationWindow
admin.site.register(MidcourseReverificationWindow)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'MidcourseReverificationWindow'
db.create_table('reverification_midcoursereverificationwindow', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('start_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
('end_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)),
))
db.send_create_signal('reverification', ['MidcourseReverificationWindow'])
def backwards(self, orm):
# Deleting model 'MidcourseReverificationWindow'
db.delete_table('reverification_midcoursereverificationwindow')
models = {
'reverification.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'})
}
}
complete_apps = ['reverification']
\ No newline at end of file
"""
Models for reverification features common to both lms and studio
"""
from datetime import datetime
import pytz
from django.core.exceptions import ValidationError
from django.db import models
from util.validate_on_save import ValidateOnSaveMixin
class MidcourseReverificationWindow(ValidateOnSaveMixin, models.Model):
"""
Defines the start and end times for midcourse reverification for a particular course.
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
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,
end_date__gte=self.start_date,
start_date__lte=self.end_date
)
if query.count() > 0:
raise ValidationError('Reverification windows cannot overlap for a given course.')
@classmethod
def window_open_for_course(cls, course_id):
"""
Returns a boolean, True if the course is currently asking for reverification, else False.
"""
now = datetime.now(pytz.UTC)
return cls.get_window(course_id, now) is not None
@classmethod
def get_window(cls, course_id, date):
"""
Returns the window that is open for a particular course for a particular date.
If no such window is open, or if more than one window is open, returns None.
"""
try:
return cls.objects.get(course_id=course_id, start_date__lte=date, end_date__gte=date)
except cls.DoesNotExist:
return None
"""
verify_student factories
"""
from reverification.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)
"""
Tests for Reverification models
"""
from datetime import timedelta, datetime
import pytz
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.test.utils import override_settings
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from reverification.models import MidcourseReverificationWindow
from reverification.tests.factories import MidcourseReverificationWindowFactory
from xmodule.modulestore.tests.factories import CourseFactory
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidcourseReverificationWindow(TestCase):
""" Tests for MidcourseReverificationWindow objects """
def setUp(self):
course = CourseFactory.create()
self.course_id = course.id
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))
)
def test_no_overlapping_windows(self):
window_valid = MidcourseReverificationWindow(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
window_valid.save()
with self.assertRaises(ValidationError):
window_invalid = MidcourseReverificationWindow(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=2),
end_date=datetime.now(pytz.utc) + timedelta(days=4)
)
window_invalid.save()
"""
Utility functions for validating forms
"""
from django import forms from django import forms
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.forms import PasswordResetForm
......
...@@ -10,6 +10,8 @@ import string # pylint: disable=W0402 ...@@ -10,6 +10,8 @@ import string # pylint: disable=W0402
import urllib import urllib
import uuid import uuid
import time import time
from collections import defaultdict
from pytz import UTC
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout, authenticate, login from django.contrib.auth import logout, authenticate, login
...@@ -45,7 +47,7 @@ from student.models import ( ...@@ -45,7 +47,7 @@ from student.models import (
) )
from student.forms import PasswordResetFormNoActive from student.forms import PasswordResetFormNoActive
from verify_student.models import SoftwareSecurePhotoVerification 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
...@@ -82,6 +84,7 @@ log = logging.getLogger("edx.student") ...@@ -82,6 +84,7 @@ log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number date status display') # pylint: disable=C0103
def csrf_token(context): def csrf_token(context):
...@@ -181,6 +184,88 @@ def cert_info(user, course): ...@@ -181,6 +184,88 @@ def cert_info(user, course):
return _cert_info(user, course, certificate_status_for_student(user, course.id)) return _cert_info(user, course, certificate_status_for_student(user, course.id))
def reverification_info(course_enrollment_pairs, user, statuses):
"""
Returns reverification-related information for *all* of user's enrollments whose
reverification status is in status_list
Args:
course_enrollment_pairs (list): list of (course, enrollment) tuples
user (User): the user whose information we want
statuses (list): a list of reverification statuses we want information for
example: ["must_reverify", "denied"]
Returns:
dictionary of lists: dictionary with one key per status, e.g.
dict["must_reverify"] = []
dict["must_reverify"] = [some information]
"""
reverifications = defaultdict(list)
for (course, enrollment) in course_enrollment_pairs:
info = single_course_reverification_info(user, course, enrollment)
if info:
reverifications[info.status].append(info)
# Sort the data by the reverification_end_date
for status in statuses:
if reverifications[status]:
reverifications[status].sort(key=lambda x: x.date)
return reverifications
def single_course_reverification_info(user, course, enrollment): # pylint: disable=invalid-name
"""Returns midcourse reverification-related information for user with enrollment in course.
If a course has an open re-verification window, and that user has a verified enrollment in
the course, we return a tuple with relevant information. Returns None if there is no info..
Args:
user (User): the user we want to get information for
course (Course): the course in which the student is enrolled
enrollment (CourseEnrollment): the object representing the type of enrollment user has in course
Returns:
ReverifyInfo: (course_id, course_name, course_number, date, status)
OR, None: None if there is no re-verification info for this enrollment
"""
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
# If there's no window OR the user is not verified, we don't get reverification info
if (not window) or (enrollment.mode != "verified"):
return None
return ReverifyInfo(
course.id, course.display_name, course.number,
window.end_date.strftime('%B %d, %Y %X %p'),
SoftwareSecurePhotoVerification.user_status(user, window)[0],
SoftwareSecurePhotoVerification.display_status(user, window),
)
def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
"""
Get the relevant set of (Course, CourseEnrollment) pairs to be displayed on
a student's dashboard.
"""
for enrollment in CourseEnrollment.enrollments_for_user(user):
try:
course = course_from_id(enrollment.course_id)
# if we are in a Microsite, then filter out anything that is not
# attributed (by ORG) to that Microsite
if course_org_filter and course_org_filter != course.location.org:
continue
# Conversely, if we are not in a Microsite, then let's filter out any enrollments
# with courses attributed (by ORG) to Microsites
elif course.location.org in org_filter_out_set:
continue
yield (course, enrollment)
except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
def _cert_info(user, course, cert_status): def _cert_info(user, course, cert_status):
""" """
Implements the logic for cert_info -- split out for testing. Implements the logic for cert_info -- split out for testing.
...@@ -321,11 +406,6 @@ def complete_course_mode_info(course_id, enrollment): ...@@ -321,11 +406,6 @@ def complete_course_mode_info(course_id, enrollment):
def dashboard(request): def dashboard(request):
user = request.user user = request.user
# Build our (course, enrollment) list for the user, but ignore any courses that no
# longer exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu.
course_enrollment_pairs = []
# for microsites, we want to filter and only show enrollments for courses within # for microsites, we want to filter and only show enrollments for courses within
# the microsites 'ORG' # the microsites 'ORG'
course_org_filter = MicrositeConfiguration.get_microsite_configuration_value('course_org_filter') course_org_filter = MicrositeConfiguration.get_microsite_configuration_value('course_org_filter')
...@@ -338,23 +418,10 @@ def dashboard(request): ...@@ -338,23 +418,10 @@ def dashboard(request):
if course_org_filter: if course_org_filter:
org_filter_out_set.remove(course_org_filter) org_filter_out_set.remove(course_org_filter)
for enrollment in CourseEnrollment.enrollments_for_user(user): # Build our (course, enrollment) list for the user, but ignore any courses that no
try: # longer exist (because the course IDs have changed). Still, we don't delete those
course = course_from_id(enrollment.course_id) # enrollments, because it could have been a data push snafu.
course_enrollment_pairs = list(get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set))
# if we are in a Microsite, then filter out anything that is not
# attributed (by ORG) to that Microsite
if course_org_filter and course_org_filter != course.location.org:
continue
# Conversely, if we are not in a Microsite, then let's filter out any enrollments
# with courses attributed (by ORG) to Microsites
elif course.location.org in org_filter_out_set:
continue
course_enrollment_pairs.append((course, enrollment))
except ItemNotFoundError:
log.error(u"User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
...@@ -386,8 +453,13 @@ def dashboard(request): ...@@ -386,8 +453,13 @@ def dashboard(request):
) )
# Verification Attempts # Verification Attempts
# Used to generate the "you must reverify for course x" banner
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user) verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
# Gets data for midcourse reverifications, if any are necessary or have failed
statuses = ["approved", "denied", "pending", "must_reverify"]
reverifications = reverification_info(course_enrollment_pairs, user, statuses)
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())
...@@ -398,6 +470,10 @@ def dashboard(request): ...@@ -398,6 +470,10 @@ def dashboard(request):
except ExternalAuthMap.DoesNotExist: except ExternalAuthMap.DoesNotExist:
pass pass
# If there are *any* denied reverifications that have not been toggled off,
# we'll display the banner
denied_banner = any(item.display for item in reverifications["denied"])
context = {'course_enrollment_pairs': course_enrollment_pairs, context = {'course_enrollment_pairs': course_enrollment_pairs,
'course_optouts': course_optouts, 'course_optouts': course_optouts,
'message': message, 'message': message,
...@@ -408,9 +484,12 @@ def dashboard(request): ...@@ -408,9 +484,12 @@ 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,
'reverifications': reverifications,
'verification_status': verification_status, 'verification_status': verification_status,
'verification_msg': verification_msg, 'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for, 'show_refund_option_for': show_refund_option_for,
'denied_banner': denied_banner,
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
} }
return render_to_response('dashboard.html', context) return render_to_response('dashboard.html', context)
......
""" Utility mixin; forces models to validate *before* saving to db """
class ValidateOnSaveMixin(object):
"""
Forces models to call their full_clean method prior to saving
"""
def save(self, force_insert=False, force_update=False, **kwargs):
"""
Modifies the save method to call full_clean
"""
if not (force_insert or force_update):
self.full_clean()
super(ValidateOnSaveMixin, self).save(force_insert, force_update, **kwargs)
...@@ -176,14 +176,16 @@ class XQueueCertInterface(object): ...@@ -176,14 +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 enrollment_mode == GeneratedCertificate.MODES.verified and SoftwareSecurePhotoVerification.user_is_verified(student): if (mode_is_verified and user_is_verified and user_is_reverified):
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 (enrollment_mode == GeneratedCertificate.MODES.verified and not elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
SoftwareSecurePhotoVerification.user_is_verified(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
......
...@@ -2,6 +2,7 @@ import logging ...@@ -2,6 +2,7 @@ import logging
import urllib import urllib
from functools import partial from functools import partial
from collections import defaultdict
from django.conf import settings from django.conf import settings
from django.core.context_processors import csrf from django.core.context_processors import csrf
...@@ -29,6 +30,7 @@ from courseware.models import StudentModule, StudentModuleHistory ...@@ -29,6 +30,7 @@ from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment
from student.views import course_from_id, single_course_reverification_info
from util.cache import cache, cache_if_anonymous from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.modulestore import Location from xmodule.modulestore import Location
...@@ -265,7 +267,8 @@ def index(request, course_id, chapter=None, section=None, ...@@ -265,7 +267,8 @@ def index(request, course_id, chapter=None, section=None,
'fragment': Fragment(), 'fragment': Fragment(),
'staff_access': staff_access, 'staff_access': staff_access,
'masquerade': masq, 'masquerade': masq,
'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa') 'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa'),
'reverifications': fetch_reverify_banner_info(request, course_id),
} }
# Only show the chat if it's enabled by the course and in the # Only show the chat if it's enabled by the course and in the
...@@ -451,9 +454,19 @@ def course_info(request, course_id): ...@@ -451,9 +454,19 @@ def course_info(request, course_id):
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
reverifications = fetch_reverify_banner_info(request, course_id)
return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None, context = {
'course': course, 'staff_access': staff_access, 'masquerade': masq}) 'request': request,
'course_id': course_id,
'cache': None,
'course': course,
'staff_access': staff_access,
'masquerade': masq,
'reverifications': reverifications,
}
return render_to_response('courseware/info.html', context)
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -654,6 +667,7 @@ def _progress(request, course_id, student_id): ...@@ -654,6 +667,7 @@ def _progress(request, course_id, student_id):
'grade_summary': grade_summary, 'grade_summary': grade_summary,
'staff_access': staff_access, 'staff_access': staff_access,
'student': student, 'student': student,
'reverifications': fetch_reverify_banner_info(request, course_id)
} }
with grades.manual_transaction(): with grades.manual_transaction():
...@@ -662,6 +676,21 @@ def _progress(request, course_id, student_id): ...@@ -662,6 +676,21 @@ def _progress(request, course_id, student_id):
return response return response
def fetch_reverify_banner_info(request, course_id):
"""
Fetches needed context variable to display reverification banner in courseware
"""
reverifications = defaultdict(list)
user = request.user
if not user.id:
return reverifications
enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id)
course = course_from_id(course_id)
info = single_course_reverification_info(user, course, enrollment)
if info:
reverifications[info.status].append(info)
return reverifications
@login_required @login_required
def submission_history(request, course_id, student_username, location): def submission_history(request, course_id, student_username, location):
"""Render an HTML fragment (meant for inclusion elsewhere) that renders a """Render an HTML fragment (meant for inclusion elsewhere) that renders a
......
...@@ -47,7 +47,6 @@ from xmodule.modulestore.xml import XMLModuleStore ...@@ -47,7 +47,6 @@ from xmodule.modulestore.xml import XMLModuleStore
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class SysadminDashboardView(TemplateView): class SysadminDashboardView(TemplateView):
"""Base class for sysadmin dashboard views with common methods""" """Base class for sysadmin dashboard views with common methods"""
...@@ -675,7 +674,7 @@ class GitLogs(TemplateView): ...@@ -675,7 +674,7 @@ class GitLogs(TemplateView):
mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host']) mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host'])
except mongoengine.connection.ConnectionError: except mongoengine.connection.ConnectionError:
log.exception('Unable to connect to mongodb to save log, ' log.exception('Unable to connect to mongodb to save log, '
'please check MONGODB_LOG settings.') 'please check MONGODB_LOG settings.')
if course_id is None: if course_id is None:
# Require staff if not going to specific course # Require staff if not going to specific course
......
"""
Exceptions for the verify student app
"""
# (Exception Class Names are sort of self-explanatory, so skipping docstring requirement)
# pylint: disable=C0111
class WindowExpiredException(Exception):
pass
# -*- 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 field 'SoftwareSecurePhotoVerification.window'
db.add_column('verify_student_softwaresecurephotoverification', 'window',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['reverification.MidcourseReverificationWindow'], null=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'SoftwareSecurePhotoVerification.window'
db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
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'})
},
'reverification.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 0x21d4398>'", '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']"}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
}
}
complete_apps = ['verify_student']
\ No newline at end of file
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'SoftwareSecurePhotoVerification.display'
db.add_column('verify_student_softwaresecurephotoverification', 'display',
self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'SoftwareSecurePhotoVerification.display'
db.delete_column('verify_student_softwaresecurephotoverification', 'display')
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'})
},
'reverification.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'}),
'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': '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 0x3176410>'", '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']"}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
}
}
complete_apps = ['verify_student']
\ No newline at end of file
# -*- 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,
)
from reverification.tests.factories import MidcourseReverificationWindowFactory
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
import verify_student.models import verify_student.models
...@@ -208,6 +215,23 @@ class TestPhotoVerification(TestCase): ...@@ -208,6 +215,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.
...@@ -339,6 +363,37 @@ class TestPhotoVerification(TestCase): ...@@ -339,6 +363,37 @@ class TestPhotoVerification(TestCase):
status = SoftwareSecurePhotoVerification.user_status(user) status = SoftwareSecurePhotoVerification.user_status(user)
self.assertEquals(status, ('must_reverify', "No photo ID was provided.")) self.assertEquals(status, ('must_reverify', "No photo ID was provided."))
# test for correct status for reverifications
window = MidcourseReverificationWindowFactory()
reverify_status = SoftwareSecurePhotoVerification.user_status(user=user, window=window)
self.assertEquals(reverify_status, ('must_reverify', ''))
reverify_attempt = SoftwareSecurePhotoVerification(user=user, window=window)
reverify_attempt.status = 'approved'
reverify_attempt.save()
reverify_status = SoftwareSecurePhotoVerification.user_status(user=user, window=window)
self.assertEquals(reverify_status, ('approved', ''))
reverify_attempt.status = 'denied'
reverify_attempt.save()
reverify_status = SoftwareSecurePhotoVerification.user_status(user=user, window=window)
self.assertEquals(reverify_status, ('denied', ''))
def test_display(self):
user = UserFactory.create()
window = MidcourseReverificationWindowFactory()
attempt = SoftwareSecurePhotoVerification(user=user, window=window, status="denied")
attempt.save()
# We expect the verification to be displayed by default
self.assertEquals(SoftwareSecurePhotoVerification.display_status(user, window), True)
# Turn it off
SoftwareSecurePhotoVerification.display_off(user.id)
self.assertEquals(SoftwareSecurePhotoVerification.display_status(user, window), False)
def test_parse_error_msg_success(self): def test_parse_error_msg_success(self):
user = UserFactory.create() user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user) attempt = SoftwareSecurePhotoVerification(user=user)
...@@ -362,3 +417,101 @@ class TestPhotoVerification(TestCase): ...@@ -362,3 +417,101 @@ 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)
@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):
""" Tests for methods that are specific to midcourse SoftwareSecurePhotoVerification objects """
def setUp(self):
self.course_id = "MITx/999/Robot_Super_Course"
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
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))
...@@ -11,6 +11,8 @@ verify_student/start?course_id=MITx/6.002x/2013_Spring # create ...@@ -11,6 +11,8 @@ verify_student/start?course_id=MITx/6.002x/2013_Spring # create
""" """
import urllib import urllib
from mock import patch, Mock, ANY from mock import patch, Mock, ANY
import pytz
from datetime import timedelta, datetime
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -18,12 +20,16 @@ from django.conf import settings ...@@ -18,12 +20,16 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from mock import sentinel
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 reverification.tests.factories import MidcourseReverificationWindowFactory
def mock_render_to_response(*args, **kwargs): def mock_render_to_response(*args, **kwargs):
...@@ -80,6 +86,8 @@ class TestReverifyView(TestCase): ...@@ -80,6 +86,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 +118,100 @@ class TestReverifyView(TestCase): ...@@ -110,3 +118,100 @@ 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')
((template, context), _kwargs) = render_mock.call_args
self.assertIn('photo_reverification', template)
self.assertTrue(context['error'])
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidCourseReverifyView(TestCase):
""" Tests for the midcourse reverification views """
def setUp(self):
self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test")
self.course_id = 'Robot/999/Test_Course'
CourseFactory.create(org='Robot', number='999', display_name='Test Course')
patcher = patch('student.models.server_track')
self.mock_server_track = patcher.start()
self.addCleanup(patcher.stop)
crum_patcher = patch('student.models.crum.get_current_request')
self.mock_get_current_request = crum_patcher.start()
self.addCleanup(crum_patcher.stop)
self.mock_get_current_request.return_value = sentinel.request
@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)
# Check that user entering the reverify flow was logged
self.mock_server_track.assert_called_once_with(
sentinel.request,
'edx.course.enrollment.reverify.started',
{
'user_id': self.user.id,
'course_id': self.course_id,
'mode': "verified",
}
)
self.mock_server_track.reset_mock()
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):
window = MidcourseReverificationWindowFactory(course_id=self.course_id)
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
response = self.client.post(url, {'face_image': ','})
# Check that submission event was logged
self.mock_server_track.assert_called_once_with(
sentinel.request,
'edx.course.enrollment.reverify.submitted',
{
'user_id': self.user.id,
'course_id': self.course_id,
'mode': "verified",
}
)
self.mock_server_track.reset_mock()
self.assertEquals(response.status_code, 302)
try:
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist:
self.fail('No verification object generated')
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_midcourse_reverify_post_failure_expired_window(self):
window = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=100),
end_date=datetime.now(pytz.UTC) - timedelta(days=50),
)
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
response = self.client.post(url, {'face_image': ','})
self.assertEquals(response.status_code, 302)
with self.assertRaises(ObjectDoesNotExist):
SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window)
@patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_dash(self):
url = reverse('verify_student_midcourse_reverify_dash')
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)
...@@ -13,7 +13,7 @@ urlpatterns = patterns( ...@@ -13,7 +13,7 @@ urlpatterns = patterns(
url( url(
r'^verify/(?P<course_id>[^/]+/[^/]+/[^/]+)$', r'^verify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.VerifyView.as_view(), views.VerifyView.as_view(), # pylint: disable=E1120
name="verify_student_verify" name="verify_student_verify"
), ),
...@@ -42,8 +42,38 @@ urlpatterns = patterns( ...@@ -42,8 +42,38 @@ urlpatterns = patterns(
), ),
url( url(
r'^midcourse_reverify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.MidCourseReverifyView.as_view(), # pylint: disable=E1120
name="verify_student_midcourse_reverify"
),
url(
r'^reverification_confirmation$', r'^reverification_confirmation$',
views.reverification_submission_confirmation, views.reverification_submission_confirmation,
name="verify_student_reverification_confirmation" name="verify_student_reverification_confirmation"
), ),
url(
r'^midcourse_reverification_confirmation$',
views.midcourse_reverification_confirmation,
name="verify_student_midcourse_reverification_confirmation"
),
url(
r'^midcourse_reverify_dash$',
views.midcourse_reverify_dash,
name="verify_student_midcourse_reverify_dash"
),
url(
r'^reverification_window_expired$',
views.reverification_window_expired,
name="verify_student_reverification_window_expired"
),
url(
r'^toggle_failed_banner_off$',
views.toggle_failed_banner_off,
name="verify_student_toggle_failed_banner_off"
),
) )
...@@ -5,6 +5,10 @@ Views for the verification flow ...@@ -5,6 +5,10 @@ Views for the verification flow
import json import json
import logging import logging
import decimal import decimal
import datetime
import crum
from track.views import server_track
from pytz import UTC
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
...@@ -22,16 +26,25 @@ from django.contrib.auth.decorators import login_required ...@@ -22,16 +26,25 @@ from django.contrib.auth.decorators import login_required
from course_modes.models import CourseMode from course_modes.models import CourseMode
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.views import course_from_id from student.views import course_from_id, reverification_info
from shoppingcart.models import Order, CertificateItem from shoppingcart.models import Order, CertificateItem
from shoppingcart.processors.CyberSource import ( from shoppingcart.processors.CyberSource import (
get_signed_purchase_params, get_purchase_endpoint get_signed_purchase_params, get_purchase_endpoint
) )
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import (
SoftwareSecurePhotoVerification,
)
from reverification.models import MidcourseReverificationWindow
import ssencrypt import ssencrypt
from xmodule.modulestore.exceptions import ItemNotFoundError
from .exceptions import WindowExpiredException
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
EVENT_NAME_USER_ENTERED_MIDCOURSE_REVERIFY_VIEW = 'edx.course.enrollment.reverify.started'
EVENT_NAME_USER_SUBMITTED_MIDCOURSE_REVERIFY = 'edx.course.enrollment.reverify.submitted'
EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE = 'edx.course.enrollment.reverify.reviewed'
class VerifyView(View): class VerifyView(View):
@method_decorator(login_required) @method_decorator(login_required)
...@@ -42,7 +55,6 @@ class VerifyView(View): ...@@ -42,7 +55,6 @@ class VerifyView(View):
- Taking the id photo - Taking the id photo
- Confirming that the photos and payment price are correct - Confirming that the photos and payment price are correct
before proceeding to payment before proceeding to payment
""" """
upgrade = request.GET.get('upgrade', False) upgrade = request.GET.get('upgrade', False)
...@@ -245,6 +257,13 @@ def results_callback(request): ...@@ -245,6 +257,13 @@ def results_callback(request):
"Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result) "Result {} not understood. Known results: PASS, FAIL, SYSTEM FAIL".format(result)
) )
# If this is a reverification, log an event
if attempt.window:
course_id = window.course_id
course = course_from_id(course_id)
course_enrollment = CourseEnrollment.get_or_create_enrollment(attempt.user, course_id)
course_enrollment.emit_event(EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE)
return HttpResponse("OK!") return HttpResponse("OK!")
...@@ -323,10 +342,136 @@ class ReverifyView(View): ...@@ -323,10 +342,136 @@ 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):
"""
The mid-course reverification view.
Needs to perform these functions:
- take new face photo
- retrieve the old id photo
- submit these photos to photo verification service
Does not need to worry about pricing
"""
@method_decorator(login_required)
def get(self, request, course_id):
"""
display this view
"""
course = course_from_id(course_id)
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id)
course_enrollment.update_enrollment(mode="verified")
course_enrollment.emit_event(EVENT_NAME_USER_ENTERED_MIDCOURSE_REVERIFY_VIEW)
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)
@method_decorator(login_required)
def post(self, request, course_id):
"""
submits the reverification to SoftwareSecure
"""
try:
now = datetime.datetime.now(UTC)
window = MidcourseReverificationWindow.get_window(course_id, now)
if window is None:
raise WindowExpiredException
attempt = SoftwareSecurePhotoVerification(user=request.user, window=window)
b64_face_image = request.POST['face_image'].split(",")[1]
attempt.upload_face_image(b64_face_image.decode('base64'))
attempt.fetch_photo_id_image()
attempt.mark_ready()
attempt.save()
attempt.submit()
course_enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id)
course_enrollment.update_enrollment(mode="verified")
course_enrollment.emit_event(EVENT_NAME_USER_SUBMITTED_MIDCOURSE_REVERIFY)
return HttpResponseRedirect(reverse('verify_student_midcourse_reverification_confirmation'))
except WindowExpiredException:
log.exception(
"User {} attempted to re-verify, but the window expired before the attempt".format(request.user.id)
)
return HttpResponseRedirect(reverse('verify_student_reverification_window_expired'))
except Exception:
log.exception(
"Could not submit verification attempt for user {}".format(request.user.id)
)
context = {
"user_full_name": request.user.profile.name,
"error": True,
}
return render_to_response("verify_student/midcourse_photo_reverification.html", context)
@login_required
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.
"""
user = request.user
course_enrollment_pairs = []
for enrollment in CourseEnrollment.enrollments_for_user(user):
try:
course_enrollment_pairs.append((course_from_id(enrollment.course_id), enrollment))
except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
statuses = ["approved", "pending", "must_reverify", "denied"]
reverifications = reverification_info(course_enrollment_pairs, user, statuses)
context = {
"user_full_name": user.profile.name,
'reverifications': reverifications,
'referer': request.META.get('HTTP_REFERER'),
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
}
return render_to_response("verify_student/midcourse_reverify_dash.html", context)
def toggle_failed_banner_off(request):
"""
Finds all denied midcourse reverifications for a user and permanently toggles
the "Reverification Failed" banner off for those verifications.
"""
user_id = request.POST.get('user_id')
SoftwareSecurePhotoVerification.display_off(user_id)
@login_required @login_required
def reverification_submission_confirmation(_request): def reverification_submission_confirmation(_request):
""" """
Shows the user a confirmation page if the submission to SoftwareSecure was successful Shows the user a confirmation page if the submission to SoftwareSecure was successful
""" """
return render_to_response("verify_student/reverification_confirmation.html") return render_to_response("verify_student/reverification_confirmation.html")
@login_required
def midcourse_reverification_confirmation(_request): # pylint: disable=C0103
"""
Shows the user a confirmation page if the submission to SoftwareSecure was successful
"""
return render_to_response("verify_student/midcourse_reverification_confirmation.html")
@login_required
def reverification_window_expired(_request):
"""
Displays an error page if a student tries to submit a reverification, but the window
for that reverification has already expired.
"""
# TODO need someone to review the copy for this template
return render_to_response("verify_student/reverification_window_expired.html")
...@@ -1068,7 +1068,12 @@ INSTALLED_APPS = ( ...@@ -1068,7 +1068,12 @@ INSTALLED_APPS = (
# Dark-launching languages # Dark-launching languages
'dark_lang', 'dark_lang',
# Microsite configuration
'microsite_configuration', 'microsite_configuration',
# Student Identity Reverification
'reverification',
) )
######################### MARKETING SITE ############################### ######################### MARKETING SITE ###############################
......
...@@ -35,15 +35,22 @@ var submitReverificationPhotos = function() { ...@@ -35,15 +35,22 @@ var submitReverificationPhotos = function() {
} }
var submitMidcourseReverificationPhotos = function() {
$('<input>').attr({
type: 'hidden',
name: 'face_image',
value: $("#face_image")[0].src,
}).appendTo("#reverify_form");
$("#reverify_form").submit();
}
var submitToPaymentProcessing = function() { var submitToPaymentProcessing = function() {
var contribution_input = $("input[name='contribution']:checked") var contribution_input = $("input[name='contribution']:checked")
var contribution = 0; var contribution = 0;
if(contribution_input.attr('id') == 'contribution-other') if(contribution_input.attr('id') == 'contribution-other') {
{
contribution = $("input[name='contribution-other-amt']").val(); contribution = $("input[name='contribution-other-amt']").val();
} }
else else {
{
contribution = contribution_input.val(); contribution = contribution_input.val();
} }
var course_id = $("input[name='course_id']").val(); var course_id = $("input[name='course_id']").val();
...@@ -276,11 +283,16 @@ $(document).ready(function() { ...@@ -276,11 +283,16 @@ $(document).ready(function() {
submitReverificationPhotos(); submitReverificationPhotos();
}); });
$("#midcourse_reverify_button").click(function() {
submitMidcourseReverificationPhotos();
});
// prevent browsers from keeping this button checked // prevent browsers from keeping this button checked
$("#confirm_pics_good").prop("checked", false) $("#confirm_pics_good").prop("checked", false)
$("#confirm_pics_good").change(function() { $("#confirm_pics_good").change(function() {
$("#pay_button").toggleClass('disabled'); $("#pay_button").toggleClass('disabled');
$("#reverify_button").toggleClass('disabled'); $("#reverify_button").toggleClass('disabled');
$("#midcourse_reverify_button").toggleClass('disabled');
}); });
......
...@@ -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;
......
...@@ -224,6 +224,15 @@ $error-color: $error-red; ...@@ -224,6 +224,15 @@ $error-color: $error-red;
$warning-color: $m-pink; $warning-color: $m-pink;
$confirm-color: $m-green; $confirm-color: $m-green;
// Notifications
$notify-banner-bg-1: rgb(56,56,56);
$notify-banner-bg-2: rgb(136,136,136);
$notify-banner-bg-3: rgb(223,223,223);
$alert-color: rgb(212, 64, 64); //rich red
$warning-color: rgb(237, 189, 60); //rich yellow
$success-color: rgb(37, 184, 90); //rich green
// ==================== // ====================
// MISC: visual horizontal rules // MISC: visual horizontal rules
...@@ -308,3 +317,8 @@ $video-thumb-url: '../images/courses/video-thumb.jpg'; ...@@ -308,3 +317,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 {
display: block;
margin-bottom: ($baseline/4);
box-shadow: 0 0 5px $shadow-d1 inset;
background: $notify-banner-bg-1;
padding: $baseline ($baseline*1.5);
&.is-hidden {
display: none;
}
// basic object
.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 {
.title {
@extend %t-title5;
@extend %t-weight4;
margin-bottom: ($baseline/4);
color: inherit;
text-transform: none;
letter-spacing: 0;
}
.copy {
@extend %t-copy-sub1;
color: inherit;
p { // nasty reset
@extend %t-copy-sub1;
color: inherit;
}
}
}
.has-actions {
.msg-content {
width: flex-grid(10,12);
}
.nav-actions {
width: flex-grid(2,12);
display: inline-block;
vertical-align: middle;
text-align: right;
.action-primary {
@extend %btn-primary-green;
}
}
}
.is-dismissable {
.msg-content {
width: flex-grid(11,12);
}
.action-dismiss {
width: flex-grid(1,12);
display: inline-block;
vertical-align: top;
text-align: right;
.button-dismiss { //ugly reset on button element
@extend %t-icon4;
background: none;
box-shadow: none;
border: none;
text-shadow: none;
color: inherit;
&:hover {
color: $action-primary-bg;
}
}
}
}
// object variations
&.urgency-high {
background: $notify-banner-bg-1;
.msg {
color: $white;
}
}
&.urgency-mid {
background: $notify-banner-bg-2;
.msg {
color: $white;
}
}
&.urgency-low {
background: $notify-banner-bg-3;
.msg {
color: $black;
}
}
&.alert {
border-top: 3px solid $alert-color;
}
&.warning {
border-top: 3px solid $warning-color;
}
&.success {
border-top: 3px solid $success-color;
}
}
// prompts
// notifications
// alerts
...@@ -133,6 +133,52 @@ ...@@ -133,6 +133,52 @@
} }
} }
} }
.reverify-status-list {
padding: 0 0 0 ($baseline/2);
margin: ($baseline/4) 0;
.status-item {
@extend %t-copy-sub2;
margin-bottom: 7px;
border-bottom: 0;
padding: 0;
[class^="icon-"] {
display: inline-block;
vertical-align: top;
margin: ($baseline/10) ($baseline/4) 0 0;
}
&.is-open [class^="icon-"] {
color: $action-primary-bg;
}
&.is-pending [class^="icon-"] {
color: $warning-color;
}
&.is-approved [class^="icon-"] {
color: $success-color;
}
&.is-denied [class^="icon-"] {
color: $alert-color;
}
.label {
@extend %text-sr;
}
.course-name {
@include line-height(12);
display: inline-block;
vertical-align: top;
width: 80%;
color: inherit;
}
}
}
} }
.news-carousel { .news-carousel {
......
...@@ -181,6 +181,7 @@ ${fragment.foot_html()} ...@@ -181,6 +181,7 @@ ${fragment.foot_html()}
% endif % endif
% if accordion: % if accordion:
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" /> <%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
% endif % endif
......
...@@ -4,13 +4,16 @@ ...@@ -4,13 +4,16 @@
<%inherit file="/main.html" /> <%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("{course.display_number_with_default} Course Info").format(course=course) | h}</%block> <%block name="pagetitle">${__("{course_number} Course Info").format(course_number=course.display_number_with_default)}</%block>
<%block name="headextra"> <%block name="headextra">
<%static:css group='style-course-vendor'/> <%static:css group='style-course-vendor'/>
<%static:css group='style-course'/> <%static:css group='style-course'/>
</%block> </%block>
<%block name="title"><title>${_("{course_number} Course Info").format(course_number=course.display_number_with_default)}</title></%block>
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='info'" /> <%include file="/courseware/course_navigation.html" args="active_page='info'" />
......
...@@ -26,6 +26,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph", ...@@ -26,6 +26,7 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
</script> </script>
</%block> </%block>
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='progress'" /> <%include file="/courseware/course_navigation.html" args="active_page='progress'" />
<section class="container"> <section class="container">
......
...@@ -23,6 +23,15 @@ ...@@ -23,6 +23,15 @@
$(this).closest('.message.is-expandable').toggleClass('is-expanded'); $(this).closest('.message.is-expandable').toggleClass('is-expanded');
} }
$("#failed-verification-button-dismiss").click(function(event) {
$.ajax({
url: "${reverse('verify_student_toggle_failed_banner_off')}",
type: "post",
data: { 'user_id': ${user.id}, }
})
$("#failed-verification-banner").addClass('is-hidden');
})
$("#upgrade-to-verified").click(function(event) { $("#upgrade-to-verified").click(function(event) {
user = $(event.target).data("user"); user = $(event.target).data("user");
course = $(event.target).data("course-id"); course = $(event.target).data("course-id");
...@@ -152,6 +161,12 @@ ...@@ -152,6 +161,12 @@
</script> </script>
</%block> </%block>
% if reverifications["must_reverify"] or reverifications["denied"]:
<section class="dashboard-banner">
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
</section>
% endif
<section class="container dashboard" id="dashboard-main" aria-hidden="false"> <section class="container dashboard" id="dashboard-main" aria-hidden="false">
%if message: %if message:
...@@ -189,6 +204,8 @@ ...@@ -189,6 +204,8 @@
<%include file='dashboard/_dashboard_status_verification.html' /> <%include file='dashboard/_dashboard_status_verification.html' />
<%include file='dashboard/_dashboard_reverification_sidebar.html' />
</ul> </ul>
</section> </section>
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
% if reverifications["must_reverify"]:
% if len(reverifications["must_reverify"]) > 1:
<div class="wrapper-msg urgency-high">
<div class="msg msg-reverify has-actions">
<div class="msg-content">
<h2 class="title">${_("You need to re-verify to continue")}</h2>
<div class="copy">
<p class="activation-message">
${_("To continue in the ID Verified track in the following courses, you need to re-verify your identity:")}
</p>
<ul class="reverify-list">
% for item in reverifications["must_reverify"]:
<li class="item">
${_('{course_name}: Re-verify by {date}').format(course_name="<strong>item.course_name</strong>", date=item.date)}
</li>
% endfor
</ul>
</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>
</div>
% elif reverifications["must_reverify"]:
<div class="wrapper-msg urgency-high">
<div class="msg msg-reverify has-actions">
<div class="msg-content">
<h2 class="title">${_("You need to re-verify to continue")}</h2>
% for item in reverifications["must_reverify"]:
<div class="copy">
<p class='activation-message'>
${_('To continue in the ID Verified track in {course_name}, you need to re-verify your identity by {date}.').format(course_name="<strong>" + item.course_name + "</strong>", date=item.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>
</div>
% endfor
%endif
%endif
%if reverifications["denied"] and denied_banner:
<div class="wrapper-msg urgency-low" id="failed-verification-banner">
<div class="msg msg-reverify is-dismissable">
<div class="msg-content">
<h2 class="title">${_("Your re-verification failed")}</h2>
% for item in reverifications["denied"]:
% if item.display:
<div class="copy">
<p class='activation-message'>
${_('Your re-verification for {course_name} failed and you are no longer eligible for a Verified Certificate. If you think this is in error, please contact us at {email}.').format(course_name="<strong>" + item.course_name+ "</strong>", email='<a class="contact-link" href="mailto:{email}">{email}</a>'.format(
email=billing_email
))}
</p>
</div>
</div>
<div class="action-dismiss">
<button class="button-dismiss" id="failed-verification-button-dismiss"><i class="icon-remove-sign"></i> <span class="sr">${_('Dismiss')}</span></button>
</div>
</div>
</div>
</div>
% endif
% endfor
%endif
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<!--TODO replace this with something a clever deisgn person approves of-->
<!--TODO replace this with a shiny loopy thing to actually print out all courses-->
% if reverifications["must_reverify"] or reverifications["pending"] or reverifications["denied"] or reverifications["approved"]:
<li class="status status-verification is-accepted">
<span class="title status-title">${_("Re-verification now open for:")}</span>
<ul class="reverify-status-list">
% if reverifications["must_reverify"]:
% for item in reverifications["must_reverify"]:
<li class="status-item is-open"><i class="icon-circle-blank"></i><span class="label">${_('Re-verify now:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["pending"]:
% for item in reverifications["pending"]:
<li class="status-item is-pending"><i class="icon-circle-blank"></i><span class="label">${_('Pending:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["denied"]:
% for item in reverifications["denied"]:
<li class="status-item is-denied"><i class="icon-remove-sign"></i><span class="label">${_('Denied:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
% if reverifications["approved"]:
% for item in reverifications["approved"]:
<li class="status-item is-approved"><i class="icon-ok"></i><span class="label">${_('Approved:')}</span> <span class="course-name"><a href="${reverse('verify_student_midcourse_reverify_dash')}">${item.course_name}</span></a></li>
% endfor
%endif
</ul>
</li>
%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
......
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">midcourse-reverification-process is-not-verified step-photos register</%block>
<%block name="title"><title>${_("Re-Verify")}</title></%block>
<%block name="js_extra">
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
<script src="${static.url('js/verify_student/photocapture.js')}"></script>
</%block>
<%block name="content">
<div id="no-webcam" style="display: none;" class="wrapper-msg wrapper-msg-activate">
<div class=" msg msg-activate">
<i class="msg-icon icon-warning-sign"></i>
<div class="msg-content">
<h3 class="title">${_("No Webcam Detected")}</h3>
<div class="copy">
<p>${_("You don't seem to have a webcam connected. Double-check that your webcam is connected and working to continue.")}</p>
</div>
</div>
</div>
</div>
<div id="no-flash" style="display: none;" class="wrapper-msg wrapper-msg-activate">
<div class=" msg msg-activate">
<i class="msg-icon icon-warning-sign"></i>
<div class="msg-content">
<h3 class="title">${_("No Flash Detected")}</h3>
<div class="copy">
<p>${_("You don't seem to have Flash installed. {a_start} Get Flash {a_end} to continue your registration.").format(a_start='<a rel="external" href="http://get.adobe.com/flashplayer/">', a_end="</a>")}</p>
</div>
</div>
</div>
</div>
%if error:
<div id="submission-error" class="wrapper-msg wrapper-msg-activate">
<div class=" msg msg-activate">
<i class="msg-icon icon-warning-sign"></i>
<div class="msg-content">
<h3 class="title">${_("Error submitting your images")}</h3>
<div class="copy">
<p>${_("Oops! Something went wrong. Please confirm your details and try again.")}</p>
</div>
</div>
</div>
</div>
%endif
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<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>
<div class="instruction">
<p>${_("Use your webcam to take a picture of your face so we can match it with your original verification.")}</p>
</div>
<div class="wrapper-task">
<div id="facecam" class="task cam">
<div class="placeholder-cam" id="face_capture_div">
<div class="placeholder-art">
<p class="copy">${_("Don't see your picture? Make sure to allow your browser to use your camera when it asks for permission.")}</p>
</div>
<video id="face_video" autoplay></video><br/>
<canvas id="face_canvas" style="display:none;" width="640" height="480"></canvas>
</div>
<div class="controls photo-controls">
<ul class="list-controls">
<li class="control control-redo" id="face_reset_button">
<a class="action action-redo" href="">
<i class="icon-undo"></i> <span class="sr">${_("Retake")}</span>
</a>
</li>
<li class="control control-do" id="face_capture_button">
<a class="action action-do" href="">
<i class="icon-camera"></i><span class="sr">${_("Take photo")}</span>
</a>
</li>
<li class="control control-approve" id="face_approve_button">
<a class="action action-approve" href="">
<i class="icon-ok"></i> <span class="sr">${_("Looks good")}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="wrapper-help">
<div class="help help-task photo-tips facetips">
<h4 class="title">${_("Tips on taking a successful photo")}</h4>
<div class="copy">
<ul class="list-help">
<li class="help-item">${_("Make sure your face is well-lit")}</li>
<li class="help-item">${_("Be sure your entire face is inside the frame")}</li>
<li class="help-item">${_("Can we match the photo you took with the one on your ID?")}</li>
<li class="help-item">${_("Once in position, use the camera button")} <span class="example">(<i class="icon-camera"></i>)</span> ${_("to capture your picture")}</li>
<li class="help-item">${_("Use the checkmark button")} <span class="example">(<i class="icon-ok"></i>)</span> ${_("once you are happy with the photo")}</li>
</ul>
</div>
</div>
<div class="help help-faq facefaq">
<h4 class="sr title">${_("Common Questions")}</h4>
<div class="copy">
<dl class="list-faq">
<dt class="faq-question">${_("Why do you need my photo?")}</dt>
<dd class="faq-answer">${_("As part of the verification process, we need your photo to confirm that you are you.")}</dd>
<dt class="faq-question">${_("What do you do with this picture?")}</dt>
<dd class="faq-answer">${_("We only use it to verify your identity. It is not displayed anywhere.")}</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="review-task review-task-name">
<h4 class="title">${_("Check Your Name")}</h4>
<div class="copy">
<p>${_("Make sure your full name on your edX account ({full_name}) matches the ID you originally submitted. We will also use this as the name on your certificate.").format(full_name="<span id='full-name'>" + user_full_name + "</span>")}</p>
</div>
<ul class="list-actions">
<li class="action action-editname">
<a class="edit-name" rel="leanModal" href="#edit-name">${_("Edit your name")}</a>
</li>
</ul>
</div>
<img id="face_image" src="" style="display: none;"/>
<nav class="nav-wizard" id="face_id_next_button_nav">
<div class="prompt-verify">
<h3 class="title">Before proceeding, please review carefully</h3>
<p class="copy"> ${_("Once you verify your photo looks good and your name is correct, you can finish your re-verification and return to your course. <strong>Note: You will not have another chance to re-verify.</strong>")}</p>
<ul class="list-actions">
<li class="action action-verify">
<input type="checkbox" name="match" id="confirm_pics_good" />
<label for="confirm_pics_good">${_("Yes! You can confirm my identity with this information.")}</label>
</li>
</ul>
</div>
<ol class="wizard-steps">
<li class="wizard-step step-proceed">
<form id="reverify_form" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }">
<input type="hidden" name="course_id" value="${course_id}">
<input class="action-primary disabled" type="button" id="midcourse_reverify_button" value="${_('Submit photos &amp; re-verify')}" name="payment">
</form>
</li>
</ol>
</nav>
</div> <!-- /view -->
</div> <!-- /wrapper-view -->
</section>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_reverification_support.html" />
</section>
</div>
<%include file="_modal_editname.html" />
</%block>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">register verification-process is-not-verified step-confirmation</%block>
<%block name="title"><title>${_("Re-Verification Submission Confirmation")}</title></%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<section class="content-confirmation">
<div class="wrapper-view">
<div class="view">
<h3 class="title">${_("Your Credentials Have Been Updated")}</h3>
<div class="instruction">
<p>${_("We have received your re-verification details and submitted them for review. Your dashboard will show the notification status once the review is complete.")}</p>
<p>${_("Please note: The professor may ask you to re-verify again at other key points in the course.")}</p>
</div>
<div class="actions-next">
<ol class="list-nav">
<li class="nav-item conditional">
<a class="action action-primary" href="${reverse('verify_student_midcourse_reverify_dash')}">${_("Complete your other re-verifications")}</a>
</li>
<li class="nav-item">
<a class="action" href="${reverse('dashboard')}">${_("Return to where you left off")}</a>
</li>
</ol>
</div>
</div> <!-- /view -->
</div> <!-- /wrapper-view -->
</section>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_reverification_support.html" />
</section>
</div>
</%block>
<%! 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")}
</title>
</%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<h2 class="title">${_("You are in the ID Verified track")}</h2>
<div class="copy">
% if reverifications["must_reverify"]: # If you have reverifications to do
% if len(reverifications["must_reverify"]) > 1: # If you have >1 reverifications
<div class="wrapper-reverify-open">
<h3 class="title">${_("You currently need to re-verify for the following courses:")}</h3>
<ul class="reverification-list">
% for item in reverifications["must_reverify"]: # for 1st
<li class="item">
<div class="course-info">
<h3 class="course-name">${item.course_name} (${item.course_number})</h3>
<p class="deadline">${_('Re-verify by {date}').format(date="<strong>" + item.date + "</strong>")}</p>
</div>
<p class="reverify-status"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': item.course_id})}">Re-verify for ${item.course_number}</a></p>
</li>
% endfor
</ul>
</div>
% else: # You only have one reverification
<div class="wrapper-reverify-open">
<h3 class="title">${_("You currently need to re-verify for the following course:")}</h3>
<ul class="reverification-list">
% for item in reverifications["must_reverify"]:
<li class="item">
<div class="course-info">
<h3 class="course-name">${item.course_name} (${item.course_number})</h3>
<p class="deadline">${_('Re-verify by {date}').format(date="<strong>" + item.date + "</strong>")}</p>
</div>
<p class="reverify-status"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': item.course_id})}">Re-verify for ${item.course_number}</a></p>
</li>
% endfor
</ul>
</div>
%endif
% else:
<div class="wrapper-reverify-open">
<p class="title">${_("You have no re-verifications at present.")}</p>
</div>
%endif
% if reverifications["pending"] or reverifications["approved"] or reverifications["denied"]:
<div class="wrapper-reverify-status">
<h3 class="title">${_("The status of your submitted re-verifications:")}</h3>
<ul class="reverification-list reverification-status">
% for item in reverifications["pending"]:
<li class="item pending">
<div class="course-info">
<h3 class="course-name">${item.course_name} (${item.course_number})</h3>
<p class="deadline">${_('Re-verify by {date}').format(date="<strong>" + item.date + "</strong>")}</p>
</div>
<p class="reverify-status pending">${_("Pending")}</p>
</li>
% endfor
% for item in reverifications["approved"]:
<li class="item complete">
<div class="course-info">
<h3 class="course-name">${item.course_name} (${item.course_number})</h3>
<p class="deadline">${_('Re-verify by {date}').format(date="<strong>" + item.date + "</strong>")}</p>
</div>
<p class="reverify-status complete">${_("Complete")}</p>
</li>
% endfor
% for item in reverifications["denied"]:
<li class="item failed">
<div class="course-info">
<h3 class="course-name">${item.course_name} (${item.course_number})</h3>
<p class="deadline">${_('Re-verify by {date}').format(date="<strong>" + item.date + "</strong>")}</p>
</div>
<p class="reverify-status">${_("Failed")}</p>
</li>
% endfor
</ul>
</div>
% endif
% if reverifications["must_reverify"]:
<p class="support">${_("Don't want to re-verify right now? {a_start}Return to where you left off{a_end}").format(
a_start='<a href="{url}">'.format(url=referer),
a_end="</a>",
)}</p>
% else:
<p class="support">${_("{a_start}Return to where you left off{a_end}").format(
a_start='<a href="{url}">'.format(url=referer),
a_end="</a>",
)}</p>
% endif
</div>
<div class="wrapper-reverification-help list-faq">
<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 <strong>will not be able to correct a failed verification</strong>. If you think there was an error in the review, please contact us at {email}').format(email='<a class="contact-link" href="mailto:{email}"">{email}</a>.'.format(email=billing_email))}</p>
</div>
</div>
</div>
</article>
</div> <!-- /wrapper-content-main -->
</section>
</div>
</%block>
<%! from django.utils.translation import ugettext as _ %>
<h2 class="title">${_("You need to re-verify to continue")}</h2>
<p class='activation-message'>
${_("To continue in the ID Verified track in {course}, you need to re-verify your identity by {date}. Go to URL.").format(email)}
</p>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="bodyclass">register verification-process is-not-verified step-confirmation</%block>
<%block name="title"><title>${_("Re-Verification Failed")}</title></%block>
<%block name="js_extra">
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script>
<script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.keybd.js')}"></script>
</%block>
<%block name="content">
<div class="container">
<section class="wrapper">
<div class="wrapper-content-main">
<article class="content-main">
<section class="content-confirmation">
<div class="wrapper-view">
<div class="view">
<h3 class="title">${_("Re-Verification Failed")}</h3>
<div class="instruction">
<p>${_("Your re-verification was submitted after the re-verification deadline, and you can no longer be re-verified.")}</p>
<p>${_("Please contact support if you believe this message to be in error.")}</p>
</div>
<ol class="list-nav">
<li class="nav-item">
<a class="action action-primary" href="${reverse('dashboard')}">${_("Return to Your Dashboard")}</a>
</li>
</ol>
</div> <!-- /view -->
</div> <!-- /wrapper-view -->
</section>
</article>
</div> <!-- /wrapper-content-main -->
<%include file="_reverification_support.html" />
</section>
</div>
</%block>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment