Skip to content
Snippets Groups Projects
Commit 6c7d715e authored by Frances Botsford's avatar Frances Botsford Committed by Julia Hansbrough
Browse files

re-verification dashboard styles

parent 85030467
No related merge requests found
Showing
with 866 additions and 488 deletions
......@@ -10,7 +10,6 @@ import string # pylint: disable=W0402
import urllib
import uuid
import time
import datetime
from pytz import UTC
from django.conf import settings
......@@ -47,7 +46,7 @@ from student.models import (
)
from student.forms import PasswordResetFormNoActive
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification
from verify_student.models import SoftwareSecurePhotoVerification, MidcourseReverificationWindow
from certificates.models import CertificateStatuses, certificate_status_for_student
from xmodule.course_module import CourseDescriptor
......@@ -394,21 +393,24 @@ def dashboard(request):
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
# TODO: Factor this out into a function; I'm pretty sure there's code duplication floating around...
prompt_midcourse_reverify = False
reverify_course_data = []
for (course, enrollment) in course_enrollment_pairs:
if MidcourseReverificationWindow.window_open_for_course(course.id) and not SSPMidcourseReverification.user_has_valid_or_pending(user, course.id):
# IF the reverification window is open
if (MidcourseReverificationWindow.window_open_for_course(course.id)):
# AND the user is actually verified-enrolled AND they don't have a pending reverification already
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
status_for_window = SSPMidcourseReverification.get_status_for_window(user, window)
reverify_course_data.append(
(
course.id,
course.display_name,
window.end_date,
"must_reverify" # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure)
if (enrollment.mode == "verified" and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(user, window=window)):
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
status_for_window = SoftwareSecurePhotoVerification.user_status(user, window=window)
reverify_course_data.append(
(
course.id,
course.display_name,
window.end_date,
"must_reverify" # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure)
)
)
)
prompt_midcourse_reverify = True
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if _enrollment.refundable())
......@@ -430,7 +432,6 @@ def dashboard(request):
'all_course_modes': course_modes,
'cert_statuses': cert_statuses,
'show_email_settings_for': show_email_settings_for,
'prompt_midcourse_reverify': prompt_midcourse_reverify,
'reverify_course_data': reverify_course_data,
'verification_status': verification_status,
'verification_msg': verification_msg,
......
......@@ -176,21 +176,16 @@ class XQueueCertInterface(object):
is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists()
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
mode_is_verified = (enrollment_mode == GeneratedCertificate.MODES.verified)
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
user_is_reverified = SoftwareSecurePhotoVerification.user_is_reverified_for_all(course_id, student)
org = course_id.split('/')[0]
course_num = course_id.split('/')[1]
cert_mode = enrollment_mode
if (
(enrollment_mode == GeneratedCertificate.MODES.verified) and
SoftwareSecurePhotoVerification.user_is_verified(student) and
SSPMidcourseReverification.user_is_reverified_for_all(course_id, student)
):
if (mode_is_verified and user_is_verified and user_is_reverified):
template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
org, course_num)
elif (
(enrollment_mode == GeneratedCertificate.MODES.verified) and not
(SoftwareSecurePhotoVerification.user_is_verified(student)) and not
(SSPMidcourseReverification.user_is_reverified_for_all(course_id, student))
):
elif (mode_is_verified and not (user_is_verified and user_is_reverified)):
template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num)
cert_mode = GeneratedCertificate.MODES.honor
......
......@@ -3,4 +3,4 @@ from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.models import MidcourseReverificationWindow
admin.site.register(SoftwareSecurePhotoVerification)
admin.site.register(MidcourseReverificationWindow)
\ No newline at end of file
admin.site.register(MidcourseReverificationWindow)
......@@ -17,11 +17,19 @@ class Migration(SchemaMigration):
))
db.send_create_signal('verify_student', ['MidcourseReverificationWindow'])
# Adding field 'SoftwareSecurePhotoVerification.window'
db.add_column('verify_student_softwaresecurephotoverification', 'window',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.MidcourseReverificationWindow'], null=True),
keep_default=False)
def backwards(self, orm):
# Deleting model 'MidcourseReverificationWindow'
db.delete_table('verify_student_midcoursereverificationwindow')
# Deleting field 'SoftwareSecurePhotoVerification.window'
db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
models = {
'auth.group': {
......@@ -77,14 +85,15 @@ class Migration(SchemaMigration):
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1fdbb90>'", 'max_length': '255', 'db_index': 'True'}),
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1d47320>'", 'max_length': '255', 'db_index': 'True'}),
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.MidcourseReverificationWindow']", 'null': 'True'})
}
}
......
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'SSPMidcourseReverification'
db.create_table('verify_student_sspmidcoursereverification', (
('softwaresecurephotoverification_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['verify_student.SoftwareSecurePhotoVerification'], unique=True, primary_key=True)),
('window', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.MidcourseReverificationWindow'])),
))
db.send_create_signal('verify_student', ['SSPMidcourseReverification'])
def backwards(self, orm):
# Deleting model 'SSPMidcourseReverification'
db.delete_table('verify_student_sspmidcoursereverification')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'verify_student.midcoursereverificationwindow': {
'Meta': {'object_name': 'MidcourseReverificationWindow'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
},
'verify_student.softwaresecurephotoverification': {
'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'<function uuid4 at 0x1189320>'", 'max_length': '255', 'db_index': 'True'}),
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'verify_student.sspmidcoursereverification': {
'Meta': {'ordering': "['-created_at']", 'object_name': 'SSPMidcourseReverification', '_ormbases': ['verify_student.SoftwareSecurePhotoVerification']},
'softwaresecurephotoverification_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'unique': 'True', 'primary_key': 'True'}),
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.MidcourseReverificationWindow']"})
}
}
complete_apps = ['verify_student']
\ No newline at end of file
This diff is collapsed.
"""
verify_student factories
"""
from verify_student.models import MidcourseReverificationWindow
from factory.django import DjangoModelFactory
import pytz
from datetime import timedelta, datetime
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232
class MidcourseReverificationWindowFactory(DjangoModelFactory):
""" Creates a generic MidcourseReverificationWindow. """
FACTORY_FOR = MidcourseReverificationWindow
course_id = u'MITx/999/Robot_Super_Course'
# By default this factory creates a window that is currently open
start_date = datetime.now(pytz.UTC) - timedelta(days=100)
end_date = datetime.now(pytz.UTC) + timedelta(days=100)
# -*- coding: utf-8 -*-
from datetime import timedelta
from datetime import timedelta, datetime
import json
from xmodule.modulestore.tests.factories import CourseFactory
from nose.tools import (
assert_in, assert_is_none, assert_equals, assert_not_equals, assert_raises,
assert_true, assert_false
)
from mock import MagicMock, patch
import pytz
from django.test import TestCase
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from django.test.utils import override_settings
from django.conf import settings
import requests
import requests.exceptions
from student.tests.factories import UserFactory
from verify_student.models import SoftwareSecurePhotoVerification, VerificationException
from verify_student.models import (
SoftwareSecurePhotoVerification, VerificationException,
MidcourseReverificationWindow,
)
from verify_student.tests.factories import MidcourseReverificationWindowFactory
from util.testing import UrlResetMixin
import verify_student.models
......@@ -208,6 +216,23 @@ class TestPhotoVerification(TestCase):
return attempt
def test_fetch_photo_id_image(self):
user = UserFactory.create()
orig_attempt = SoftwareSecurePhotoVerification(user=user, window=None)
orig_attempt.save()
old_key = orig_attempt.photo_id_key
window = MidcourseReverificationWindowFactory(
course_id="ponies",
start_date=datetime.now(pytz.utc) - timedelta(days=5),
end_date=datetime.now(pytz.utc) + timedelta(days=5)
)
new_attempt = SoftwareSecurePhotoVerification(user=user, window=window)
new_attempt.save()
new_attempt.fetch_photo_id_image()
assert_equals(new_attempt.photo_id_key, old_key)
def test_submissions(self):
"""Test that we set our status correctly after a submission."""
# Basic case, things go well.
......@@ -362,3 +387,146 @@ class TestPhotoVerification(TestCase):
attempt.error_msg = msg
parsed_error_msg = attempt.parsed_error_msg()
self.assertEquals(parsed_error_msg, "There was an error verifying your ID photos.")
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidcourseReverificationWindow(TestCase):
""" Tests for MidcourseReverificationWindow objects """
def setUp(self):
self.course_id = "MITx/999/Robot_Super_Course"
CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
def test_window_open_for_course(self):
# Should return False if no windows exist for a course
self.assertFalse(MidcourseReverificationWindow.window_open_for_course(self.course_id))
# Should return False if a window exists, but it's not in the current timeframe
MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=10),
end_date=datetime.now(pytz.utc) - timedelta(days=5)
)
self.assertFalse(MidcourseReverificationWindow.window_open_for_course(self.course_id))
# Should return True if a non-expired window exists
MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
self.assertTrue(MidcourseReverificationWindow.window_open_for_course(self.course_id))
def test_get_window(self):
# if no window exists, returns None
self.assertIsNone(MidcourseReverificationWindow.get_window(self.course_id, datetime.now(pytz.utc)))
# we should get the expected window otherwise
window_valid = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
self.assertEquals(
window_valid,
MidcourseReverificationWindow.get_window(self.course_id, datetime.now(pytz.utc))
)
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@patch('verify_student.models.S3Connection', new=MockS3Connection)
@patch('verify_student.models.Key', new=MockKey)
@patch('verify_student.models.requests.post', new=mock_software_secure_post)
class TestMidcourseReverification(TestCase):
def setUp(self):
self.course_id = "MITx/999/Robot_Super_Course"
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
self.user = UserFactory.create()
def test_user_is_reverified_for_all(self):
# if there are no windows for a course, this should return True
self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
# first, make three windows
window1 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
window2 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=10),
end_date=datetime.now(pytz.UTC) - timedelta(days=8),
)
window3 = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=5),
end_date=datetime.now(pytz.UTC) - timedelta(days=3),
)
# make two SSPMidcourseReverifications for those windows
attempt1 = SoftwareSecurePhotoVerification(
status="approved",
user=self.user,
window=window1
)
attempt1.save()
attempt2 = SoftwareSecurePhotoVerification(
status="approved",
user=self.user,
window=window2
)
attempt2.save()
# should return False because only 2 of 3 windows have verifications
self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
attempt3 = SoftwareSecurePhotoVerification(
status="must_retry",
user=self.user,
window=window3
)
attempt3.save()
# should return False because the last verification exists BUT is not approved
self.assertFalse(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
attempt3.status = "approved"
attempt3.save()
# should now return True because all windows have approved verifications
self.assertTrue(SoftwareSecurePhotoVerification.user_is_reverified_for_all(self.course_id, self.user))
def test_original_verification(self):
orig_attempt = SoftwareSecurePhotoVerification(user=self.user)
orig_attempt.save()
window = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
midcourse_attempt = SoftwareSecurePhotoVerification(user=self.user, window=window)
self.assertEquals(midcourse_attempt.original_verification(user=self.user), orig_attempt)
def test_user_has_valid_or_pending(self):
window = MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.UTC) - timedelta(days=15),
end_date=datetime.now(pytz.UTC) - timedelta(days=13),
)
attempt = SoftwareSecurePhotoVerification(status="must_retry", user=self.user, window=window)
attempt.save()
assert_false(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user=self.user, window=window))
attempt.status = "approved"
attempt.save()
assert_true(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user=self.user, window=window))
def test_active_for_user(self):
pass
......@@ -21,9 +21,11 @@ from django.core.exceptions import ObjectDoesNotExist
from xmodule.modulestore.tests.factories import CourseFactory
from courseware.tests.tests import TEST_DATA_MONGO_MODULESTORE
from student.tests.factories import UserFactory
from student.models import CourseEnrollment
from course_modes.models import CourseMode
from verify_student.views import render_to_response
from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.tests.factories import MidcourseReverificationWindowFactory
def mock_render_to_response(*args, **kwargs):
......@@ -80,6 +82,8 @@ class TestReverifyView(TestCase):
def setUp(self):
self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test")
self.course_id = "MITx/999/Robot_Super_Course"
self.course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course')
@patch('verify_student.views.render_to_response', render_mock)
def test_reverify_get(self):
......@@ -110,3 +114,61 @@ class TestReverifyView(TestCase):
self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist:
self.fail('No verification object generated')
self.assertIn('photo_reverification', template)
self.assertTrue(context['error'])
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_reverify_post_success(self):
url = reverse('verify_student_reverify')
response = self.client.post(url, {'face_image': ',',
'photo_id_image': ','})
self.assertEquals(response.status_code, 302)
try:
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist:
self.fail('No verification object generated')
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
class TestMidCourseReverifyView(TestCase):
def setUp(self):
self.user = UserFactory.create(username="rusty", password="test")
self.client.login(username="rusty", password="test")
self.course_id = 'Robot/999/Test_Course'
CourseFactory.create(org='Robot', number='999', display_name='Test Course')
@patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_get(self):
url = reverse('verify_student_midcourse_reverify',
kwargs={"course_id": self.course_id})
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
((_template, context), _kwargs) = render_mock.call_args
self.assertFalse(context['error'])
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
def test_midcourse_reverify_post_success(self):
url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_id})
response = self.client.post(url, {'face_image': ','})
self.assertEquals(response.status_code, 302)
try:
verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
self.assertIsNotNone(verification_attempt)
except ObjectDoesNotExist:
self.fail('No verification object generated')
# TODO make this test more detailed
@patch('verify_student.views.render_to_response', render_mock)
def test_midcourse_reverify_dash(self):
url = reverse('verify_student_midcourse_reverify_dash')
response = self.client.get(url)
# not enrolled in any courses
self.assertEquals(response.status_code, 200)
enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_id)
enrollment.update_enrollment(mode="verified", is_active=True)
MidcourseReverificationWindowFactory(course_id=self.course_id)
response = self.client.get(url)
# enrolled in a verified course, and the window is open
self.assertEquals(response.status_code, 200)
......@@ -30,9 +30,10 @@ from shoppingcart.processors.CyberSource import (
get_signed_purchase_params, get_purchase_endpoint
)
from verify_student.models import (
SoftwareSecurePhotoVerification, MidcourseReverificationWindow, SSPMidcourseReverification
SoftwareSecurePhotoVerification, MidcourseReverificationWindow,
)
import ssencrypt
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
......@@ -326,6 +327,7 @@ class ReverifyView(View):
}
return render_to_response("verify_student/photo_reverification.html", context)
class MidCourseReverifyView(View):
"""
The mid-course reverification view.
......@@ -341,10 +343,16 @@ class MidCourseReverifyView(View):
"""
display this view
"""
course = course_from_id(course_id)
context = {
"user_full_name": request.user.profile.name,
"error": False,
"course_id": course_id,
"course_name": course.display_name_with_default,
"course_org": course.display_org_with_default,
"course_num": course.display_number_with_default,
"reverify": True,
}
return render_to_response("verify_student/midcourse_photo_reverification.html", context)
......@@ -356,7 +364,7 @@ class MidCourseReverifyView(View):
try:
# TODO look at this more carefully! #1 testing candidate
now = datetime.datetime.now(UTC)
attempt = SSPMidcourseReverification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now))
attempt = SoftwareSecurePhotoVerification(user=request.user, window=MidcourseReverificationWindow.get_window(course_id, now))
b64_face_image = request.POST['face_image'].split(",")[1]
attempt.upload_face_image(b64_face_image.decode('base64'))
......@@ -376,7 +384,12 @@ class MidCourseReverifyView(View):
}
return render_to_response("verify_student/midcourse_photo_reverification.html", context)
def midcourse_reverify_dash(_request):
"""
Shows the "course reverification dashboard", which displays the reverification status (must reverify,
pending, approved, failed, etc) of all courses in which a student has a verified enrollment.
"""
# TODO same comment as in student/views.py: need to factor out this functionality
user = _request.user
course_enrollment_pairs = []
......@@ -397,13 +410,13 @@ def midcourse_reverify_dash(_request):
"must_reverify"
)
)
prompt_midcourse_reverify = True
context = {
"user_full_name": _request.user.profile.name,
"reverify_course_data": reverify_course_data,
}
return render_to_response("verify_student/midcourse_reverify_dash.html", context)
@login_required
def reverification_submission_confirmation(_request):
"""
......@@ -411,6 +424,7 @@ def reverification_submission_confirmation(_request):
"""
return render_to_response("verify_student/reverification_confirmation.html")
@login_required
def midcourse_reverification_confirmation(_request):
"""
......
// TODO diff this against photocapture.js, see if I actually needed a whole honking new file
var onVideoFail = function(e) {
if(e == 'NO_DEVICES_FOUND') {
$('#no-webcam').show();
......@@ -25,7 +24,7 @@ var submitReverificationPhotos = function() {
name: 'face_image',
value: $("#face_image")[0].src,
}).appendTo("#reverify_form");
// there is a change here
$("#reverify_form").submit();
}
......@@ -48,6 +47,7 @@ var submitToPaymentProcessing = function() {
"course_id" : course_id,
"contribution": contribution,
"face_image" : $("#face_image")[0].src,
// there is a change here
},
function(data) {
for (prop in data) {
......
......@@ -12,8 +12,8 @@
// base - utilities
@import 'base/reset';
@import 'base/mixins';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
......
......@@ -12,8 +12,8 @@
// base - utilities
@import 'base/reset';
@import 'base/mixins';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
......@@ -41,6 +41,7 @@
// base - elements
@import 'elements/typography';
@import 'elements/controls';
@import 'elements/system-feedback';
// base - specific views
@import 'views/verification';
......
......@@ -11,8 +11,8 @@
// base - utilities
@import 'base/reset';
@import 'base/mixins';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
......
......@@ -54,6 +54,30 @@
// ====================
// extends - UI - used for page/view-level wrappers (for centering/grids)
%ui-wrapper {
@include clearfix();
@include box-sizing(border-box);
width: 100%;
}
// extends - UI - window
%ui-window {
@include clearfix();
border-radius: 3px;
box-shadow: 0 1px 2px 1px $shadow-l1;
margin-bottom: $baseline;
border: 1px solid $light-gray;
background: $white;
}
// extends - UI archetypes - well
%ui-well {
box-shadow: inset 0 1px 2px 1px $shadow-l1;
padding: ($baseline*0.75) $baseline;
}
// extends - UI - visual link
%ui-fake-link {
cursor: pointer;
......
......@@ -308,3 +308,8 @@ $video-thumb-url: '../images/courses/video-thumb.jpg';
$f-serif: 'Bree Serif', Georgia, Cambria, 'Times New Roman', Times, serif;
$f-sans-serif: 'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif;
$f-monospace: 'Bitstream Vera Sans Mono', Consolas, Courier, monospace;
// SPLINT: colors
$msg-bg: $action-primary-bg;
......@@ -2,8 +2,8 @@
@import 'base/reset';
@import 'base/font_face';
@import 'base/mixins';
@import 'base/variables';
@import 'base/mixins';
## THEMING
## -------
......
// lms - elements - system feedback
// ====================
// messages
// UI : message
.wrapper-msg {
box-shadow: 0 0 5px $action-primary-shadow inset;
margin-bottom: ($baseline*1.5);
padding: $baseline ($baseline*1.5);
background: $action-primary-bg;
.msg {
@include clearfix();
max-width: grid-width(12);
min-width: 760px;
width: flex-grid(12);
margin: 0 auto;
}
.msg-content,
.msg-icon {
display: inline-block;
vertical-align: middle;
}
.msg-content {
width: flex-grid(10,12);
.title {
@extend %t-title5;
@extend %t-weight4;
margin-bottom: ($baseline/4);
color: $white;
text-transform: none;
letter-spacing: 0;
}
.copy {
@extend %t-copy-sub1;
color: $white;
p { // nasty reset
@extend %t-copy-sub1;
color: $white;
}
}
}
.has-actions {
.msg-content {
width: flex-grid(10,12);
}
.nav-actions {
width: flex-grid(2,12);
display: inline-block;
vertical-align: middle;
.action-primary {
@extend %btn-primary-green;
}
}
}
}
// prompts
// notifications
// alerts
// lms - views - verification flow
// ====================
// MISC: extends - type
// application: canned headings
%hd-lv1 {
@extend %t-title1;
@extend %t-weight1;
color: $m-gray-d4;
margin: 0 0 ($baseline*2) 0;
}
%hd-lv2 {
@extend %t-title4;
@extend %t-weight1;
margin: 0 0 ($baseline*0.75) 0;
border-bottom: 1px solid $m-gray-l4;
padding-bottom: ($baseline/2);
color: $m-gray-d4;
}
%hd-lv3 {
@extend %t-title6;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
%hd-lv4 {
@extend %t-title6;
@extend %t-weight2;
margin: 0 0 $baseline 0;
color: $m-gray-d4;
}
%hd-lv5 {
@extend %t-title7;
@extend %t-weight4;
margin: 0 0 ($baseline/4) 0;
color: $m-gray-d4;
}
// application: canned copy
%copy-base {
@extend %t-copy-base;
color: $m-gray-d2;
}
%copy-lead1 {
@extend %t-copy-lead2;
color: $m-gray;
}
%copy-detail {
@extend %t-copy-sub1;
@extend %t-weight3;
color: $m-gray-d1;
}
%copy-metadata {
@extend %t-copy-sub2;
color: $m-gray-d1;
%copy-metadata-value {
@extend %t-weight2;
}
%copy-metadata-value {
@extend %t-weight4;
}
}
// application: canned links
%copy-link {
border-bottom: 1px dotted transparent;
&:hover, &:active {
border-color: $link-color-d1;
}
}
// ====================
// MISC: extends - button
%btn-verify-primary {
@extend %btn-primary-green;
......@@ -89,26 +8,6 @@
// ====================
// MISC: extends - UI - window
%ui-window {
@include clearfix();
border-radius: ($baseline/10);
box-shadow: 0 1px 2px 1px $shadow-l1;
margin-bottom: $baseline;
border: 1px solid $m-gray-l3;
background: $white;
}
// ====================
// MISC: extends - UI - well
%ui-well {
box-shadow: inset 0 1px 2px 1px $shadow-l1;
padding: ($baseline*0.75) $baseline;
}
// ====================
// MISC: expandable UI
.is-expandable {
......@@ -153,7 +52,8 @@
// ====================
// VIEW: all verification steps
.verification-process {
.verification-process,
.midcourse-reverification-process {
// reset: box-sizing (making things so right its scary)
* {
......@@ -1894,6 +1794,335 @@
}
}
}
// VIEW: midcourse re-verification
&.midcourse-reverification-process {
// step-dash
.action-reverify {
@extend %btn-verify-primary;
padding: ($baseline/2) ($baseline*0.75);
}
.reverification-table {
width: 100%;
th {
display: none;
}
th,
td {
padding: ($baseline/2) 0;
text-align: left;
border-bottom: 1px solid $light-gray;
}
.course-name {
@extend %t-title5;
display: block;
font-weight: bold;
}
.deadline {
@extend %copy-detail;
display: block;
}
}
.wrapper-reverification-help {
margin-top: $baseline*2;
.faq-item {
display: inline-block;
vertical-align: top;
width: flex-grid(4,12);
padding-right: $baseline;
&:last-child {
padding-right: 0;
}
.faq-answer {
@extend %t-copy-sub1;
}
}
}
// step-photos
.wrapper-task {
@include clearfix();
width: flex-grid(12,12);
margin: $baseline 0;
.wrapper-help {
float: right;
width: flex-grid(6,12);
padding: 0 $baseline;
.help {
margin-bottom: ($baseline*1.5);
&:last-child {
margin-bottom: 0;
}
.title {
@extend %hd-lv3;
}
.copy {
@extend %copy-detail;
}
.example {
color: $m-gray-l2;
}
// help - general list
.list-help {
margin-top: ($baseline/2);
color: $black;
.help-item {
margin-bottom: ($baseline/4);
border-bottom: 1px solid $m-gray-l4;
padding-bottom: ($baseline/4);
&:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 0;
}
}
.help-item-emphasis {
@extend %t-weight4;
}
}
// help - faq
.list-faq {
margin-bottom: $baseline;
}
}
}
.task {
@extend %ui-window;
float: left;
width: flex-grid(6,12);
margin-right: flex-gutter();
}
.controls {
padding: ($baseline*0.75) $baseline;
background: $m-gray-l4;
.list-controls {
position: relative;
}
.control {
position: absolute;
.action {
@extend %btn-primary-blue;
padding: ($baseline/2) ($baseline*0.75);
*[class^="icon-"] {
@extend %t-icon4;
padding: ($baseline*.25) ($baseline*.5);
display: block;
}
}
// STATE: hidden
&.is-hidden {
visibility: hidden;
}
// STATE: shown
&.is-shown {
visibility: visible;
}
// STATE: approved
&.approved {
.action {
@extend %btn-verify-primary;
padding: ($baseline/2) ($baseline*0.75);
}
}
}
// control - redo
.control-redo {
position: absolute;
left: ($baseline/2);
}
// control - take/do
.control-do {
left: 45%;
}
// control - approve
.control-approve {
position: absolute;
right: ($baseline/2);
}
}
.msg {
@include clearfix();
margin-top: ($baseline*2);
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.list-actions {
position: relative;
top: -($baseline/2);
float: left;
width: flex-grid(4,12);
text-align: right;
.action-retakephotos a {
@extend %btn-primary-blue;
@include font-size(14);
padding: ($baseline/2) ($baseline*.75);
}
}
}
.msg-followup {
border-top: ($baseline/10) solid $m-gray-t0;
padding-top: $baseline;
}
}
.review-task {
margin-bottom: ($baseline*1.5);
padding: ($baseline*0.75) $baseline;
border-radius: ($baseline/10);
background: $m-gray-l4;
&:last-child {
margin-bottom: 0;
}
> .title {
@extend %hd-lv3;
}
.copy {
@extend %copy-base;
strong {
@extend %t-weight5;
color: $m-gray-d4;
}
}
}
// individual task - name
.review-task-name {
@include clearfix();
.copy {
float: left;
width: flex-grid(8,12);
margin-right: flex-gutter();
}
.list-actions {
position: relative;
top: -($baseline);
float: left;
width: flex-grid(4,12);
text-align: right;
.action-editname a {
@extend %btn-primary-blue;
@include font-size(14);
padding: ($baseline/2) ($baseline*.75);
}
}
}
.nav-wizard {
padding: ($baseline*.75) $baseline;
.prompt-verify {
float: left;
width: flex-grid(6,12);
margin: 0 flex-gutter() 0 0;
.title {
@extend %hd-lv4;
margin-bottom: ($baseline/4);
}
.copy {
@extend %t-copy-sub1;
@extend %t-weight3;
}
.list-actions {
margin-top: ($baseline/2);
}
.action-verify label {
@extend %t-copy-sub1;
}
}
.wizard-steps {
margin-top: ($baseline/2);
.wizard-step {
margin-right: flex-gutter();
display: inline-block;
vertical-align: middle;
&:last-child {
margin-right: 0;
}
}
}
}
.modal {
fieldset {
margin-top: $baseline;
}
.close-modal {
@include font-size(24);
color: $m-blue-d3;
&:hover, &:focus {
color: $m-blue-d1;
border: none;
}
}
}
}
}
// ====================
......
......@@ -152,14 +152,17 @@
</script>
</%block>
<section class="container dashboard" id="dashboard-main" aria-hidden="false">
<!-- TODO later will need to make this ping for all courses on the dash -->
%if prompt_midcourse_reverify:
<section class="dashboard-banner">
<!-- TODO later will need to make this ping for all courses on the dash -->
%if reverify_course_data:
<section class="dashboard-banner">
<div class="wrapper-msg">
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
</section>
% endif
</div>
</section>
% endif
<section class="container dashboard" id="dashboard-main" aria-hidden="false">
%if message:
<section class="dashboard-banner">
......
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