Commit 0b759417 by Julia Hansbrough

Moved reverification windows into common

Added verification sidebar, banner for major courseware sections, quality & test improvements
parent 003d8234
......@@ -451,9 +451,12 @@ INSTALLED_APPS = (
# for managing course modes
'course_modes',
<<<<<<< HEAD
# Dark-launching languages
'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 verify_student.models import MidcourseReverificationWindow
from reverification.models import MidcourseReverificationWindow
from factory.django import DjangoModelFactory
import pytz
from datetime import timedelta, datetime
......
"""
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):
MidcourseReverificationWindowFactory(
course_id=self.course_id,
start_date=datetime.now(pytz.utc) - timedelta(days=3),
end_date=datetime.now(pytz.utc) + timedelta(days=3)
)
with self.assertRaises(ValidationError):
window_invalid = MidcourseReverificationWindowFactory(
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.contrib.auth.models import User
from django.contrib.auth.forms import PasswordResetForm
......
......@@ -184,28 +184,34 @@ def cert_info(user, course):
def reverification_info(user, course, enrollment):
"""
If "user" currently needs to be reverified in "course", this returns a four-tuple with re-verification
info for views to display. Else, returns None.
If
- a course has an open re-verification window, and
- that user has a verified enrollment in the course
then we return a tuple with relevant information.
Four-tuple data: (course_id, course_display_name, reverification_end_date, reverification_status)
Else, return None.
Five-tuple data: (course_id, course_display_name, course_number, reverification_end_date, reverification_status)
"""
# IF the reverification window is open
if (MidcourseReverificationWindow.window_open_for_course(course.id)):
# AND the user is actually verified-enrolled AND they don't have a pending reverification already
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
if (enrollment.mode == "verified" and not SoftwareSecurePhotoVerification.user_has_valid_or_pending(user, window=window)):
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
window = MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC))
# If there's an open window AND the user is verified
if (window and (enrollment.mode == "verified")):
return (
course.id,
course.display_name,
course.number,
window.end_date.strftime('%B %d, %Y %X %p'),
"must_reverify" # TODO: reflect more states than just "must_reverify" has_valid_or_pending (must show failure)
SoftwareSecurePhotoVerification.user_status(user, window)[0],
)
return None
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.
"""
course_enrollment_pairs = []
for enrollment in CourseEnrollment.enrollments_for_user(user):
try:
......@@ -220,11 +226,10 @@ def get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set):
elif course.location.org in org_filter_out_set:
continue
course_enrollment_pairs.append((course, enrollment))
yield (course, enrollment)
except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
return course_enrollment_pairs
......@@ -252,7 +257,6 @@ def _cert_info(user, course, cert_status):
CertificateStatuses.restricted: 'restricted',
}
# TODO: We need the thing on the sidebar to mention if reverification, as per UI flows.
status = template_state.get(cert_status['status'], default_status)
d = {'status': status,
......@@ -384,7 +388,7 @@ def dashboard(request):
# Build our (course, enrollment) list for the user, but ignore any courses that no
# longer exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu.
course_enrollment_pairs = get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set)
course_enrollment_pairs = list(get_course_enrollment_pairs(user, course_org_filter, org_filter_out_set))
course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True)
......@@ -417,15 +421,30 @@ def dashboard(request):
# Verification Attempts
# Used to generate the "you must reverify for course x" banner
# TODO: make this banner appear at the top of courseware as well
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
# Gets data for midcourse reverifications, if any are necessary or have failed
reverify_course_data = []
reverifications_must_reverify = []
reverifications_denied = []
reverifications_pending = []
reverifications_approved = []
for (course, enrollment) in course_enrollment_pairs:
info = reverification_info(user, course, enrollment)
if info:
reverify_course_data.append(info)
if "approved" in info:
reverifications_approved.append(info)
elif "pending" in info:
reverifications_pending.append(info)
elif "must_reverify" in info:
reverifications_must_reverify.append(info)
elif "denied" in info:
reverifications_denied.append(info)
# Sort the data by the reverification_end_date
reverifications_must_reverify = sorted(reverifications_must_reverify, key=lambda x: x[3])
reverifications_denied = sorted(reverifications_denied, key=lambda x: x[3])
reverifications_pending = sorted(reverifications_pending, key=lambda x: x[3])
reverifications_approved = sorted(reverifications_approved, key=lambda x: x[3])
show_refund_option_for = frozenset(course.id for course, _enrollment in course_enrollment_pairs
if _enrollment.refundable())
......@@ -447,7 +466,10 @@ def dashboard(request):
'all_course_modes': course_modes,
'cert_statuses': cert_statuses,
'show_email_settings_for': show_email_settings_for,
'reverify_course_data': reverify_course_data,
'reverifications_must_reverify': reverifications_must_reverify,
'reverifications_denied': reverifications_denied,
'reverifications_pending': reverifications_pending,
'reverifications_approved': reverifications_approved,
'verification_status': verification_status,
'verification_msg': verification_msg,
'show_refund_option_for': show_refund_option_for,
......
""" 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)
......@@ -29,6 +29,7 @@ from courseware.models import StudentModule, StudentModuleHistory
from course_modes.models import CourseMode
from student.models import UserTestGroup, CourseEnrollment
from student.views import course_from_id, reverification_info
from util.cache import cache, cache_if_anonymous
from xblock.fragment import Fragment
from xmodule.modulestore import Location
......@@ -267,6 +268,8 @@ def index(request, course_id, chapter=None, section=None,
'masquerade': masq,
'xqa_server': settings.FEATURES.get('USE_XQA_SERVER', 'http://xqa:server@content-qa.mitx.mit.edu/xqa')
}
reverify_context = fetch_reverify_banner_keypairs(request, course_id)
context = dict(context.items() + reverify_context.items())
# Only show the chat if it's enabled by the course and in the
# settings.
......@@ -452,8 +455,12 @@ def course_info(request, course_id):
staff_access = has_access(request.user, course, 'staff')
masq = setup_masquerade(request, staff_access) # allow staff to toggle masquerade on info page
return render_to_response('courseware/info.html', {'request': request, 'course_id': course_id, 'cache': None,
'course': course, 'staff_access': staff_access, 'masquerade': masq})
context = {'request': request, 'course_id': course_id, 'cache': None,
'course': course, 'staff_access': staff_access, 'masquerade': masq}
reverify_context = fetch_reverify_banner_keypairs(request, course_id)
context = dict(context.items() + reverify_context.items())
return render_to_response('courseware/info.html', context)
@ensure_csrf_cookie
......@@ -655,6 +662,9 @@ def _progress(request, course_id, student_id):
'staff_access': staff_access,
'student': student,
}
reverify_context = fetch_reverify_banner_keypairs(request, course_id)
context = dict(context.items() + reverify_context.items())
with grades.manual_transaction():
response = render_to_response('courseware/progress.html', context)
......@@ -662,6 +672,29 @@ def _progress(request, course_id, student_id):
return response
def fetch_reverify_banner_keypairs(request, course_id):
"""
Fetches needed context variables to display reverification banner in courseware
"""
reverifications_must_reverify = []
reverifications_denied = []
user = request.user
if not user.id:
return {'reverifications_must_reverify': None, 'reverifications_denied': None, }
enrollment = CourseEnrollment.get_or_create_enrollment(request.user, course_id)
course = course_from_id(course_id)
info = reverification_info(user, course, enrollment)
if info:
if "must_reverify" in info:
reverifications_must_reverify.append(info)
elif "denied" in info:
reverifications_denied.append(info)
return {
'reverifications_must_reverify': reverifications_must_reverify,
'reverifications_denied': reverifications_denied,
}
@login_required
def submission_history(request, course_id, student_username, location):
"""Render an HTML fragment (meant for inclusion elsewhere) that renders a
......
......@@ -47,7 +47,6 @@ from xmodule.modulestore.xml import XMLModuleStore
log = logging.getLogger(__name__)
class SysadminDashboardView(TemplateView):
"""Base class for sysadmin dashboard views with common methods"""
......@@ -675,7 +674,7 @@ class GitLogs(TemplateView):
mdb = mongoengine.connect(mongo_db['db'], host=mongo_db['host'])
except mongoengine.connection.ConnectionError:
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:
# Require staff if not going to specific course
......
from ratelimitbackend import admin
from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.models import MidcourseReverificationWindow
admin.site.register(SoftwareSecurePhotoVerification)
admin.site.register(MidcourseReverificationWindow)
......@@ -8,27 +8,17 @@ from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'MidcourseReverificationWindow'
db.create_table('verify_student_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('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),
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['reverification.MidcourseReverificationWindow'], null=True),
keep_default=False)
def backwards(self, orm):
# Deleting model 'MidcourseReverificationWindow'
db.delete_table('verify_student_midcoursereverificationwindow')
def backwards(self, orm):
# Deleting field 'SoftwareSecurePhotoVerification.window'
db.delete_column('verify_student_softwaresecurephotoverification', 'window_id')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
......@@ -66,7 +56,7 @@ class Migration(SchemaMigration):
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'verify_student.midcoursereverificationwindow': {
'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'}),
......@@ -83,7 +73,7 @@ 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 0x1d47320>'", 'max_length': '255', 'db_index': 'True'}),
'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'}),
......@@ -91,8 +81,8 @@ class Migration(SchemaMigration):
'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['verify_student.MidcourseReverificationWindow']", 'null': 'True'})
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
}
}
complete_apps = ['verify_student']
complete_apps = ['verify_student']
\ No newline at end of file
......@@ -23,7 +23,6 @@ import pytz
import requests
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.contrib.auth.models import User
......@@ -36,57 +35,16 @@ from verify_student.ssencrypt import (
generate_signed_message, rsa_encrypt
)
from reverification.models import MidcourseReverificationWindow
log = logging.getLogger(__name__)
def generateUUID():
def generateUUID(): # pylint: disable=C0103
""" Utility function; generates UUIDs """
return str(uuid.uuid4)
class MidcourseReverificationWindow(models.Model):
"""
Defines the start and end times for midcourse reverification for a particular course.
There can be many MidcourseReverificationWindows per course, but they 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)
for item in query:
if (self.start_date <= item.end_date) and (item.start_date <= self.end_date):
raise ValidationError('Reverification windows cannot overlap for a given course.')
@classmethod
def window_open_for_course(cls, course_id):
"""
Returns a boolean, True if the course is currently asking for reverification, else False.
"""
now = datetime.now(pytz.UTC)
if cls.get_window(course_id, now):
return True
return False
@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 Exception:
return None
class VerificationException(Exception):
pass
......@@ -571,7 +529,7 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
attempt = attempts[0]
if attempt.status != "approved":
return False
except:
except Exception: # pylint: disable=W0703
return False
return True
......
......@@ -18,9 +18,8 @@ import requests.exceptions
from student.tests.factories import UserFactory
from verify_student.models import (
SoftwareSecurePhotoVerification, VerificationException,
MidcourseReverificationWindow,
)
from verify_student.tests.factories import MidcourseReverificationWindowFactory
from reverification.tests.factories import MidcourseReverificationWindowFactory
from util.testing import UrlResetMixin
import verify_student.models
......@@ -364,6 +363,25 @@ class TestPhotoVerification(TestCase):
status = SoftwareSecurePhotoVerification.user_status(user)
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_parse_error_msg_success(self):
user = UserFactory.create()
attempt = SoftwareSecurePhotoVerification(user=user)
......@@ -390,49 +408,6 @@ class TestPhotoVerification(TestCase):
@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)
......
......@@ -27,7 +27,7 @@ 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
from reverification.tests.factories import MidcourseReverificationWindowFactory
def mock_render_to_response(*args, **kwargs):
......
......@@ -13,7 +13,7 @@ urlpatterns = patterns(
url(
r'^verify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.VerifyView.as_view(),
views.VerifyView.as_view(), # pylint: disable=E1120
name="verify_student_verify"
),
......@@ -43,7 +43,7 @@ urlpatterns = patterns(
url(
r'^midcourse_reverify/(?P<course_id>[^/]+/[^/]+/[^/]+)$',
views.MidCourseReverifyView.as_view(),
views.MidCourseReverifyView.as_view(), # pylint: disable=E1120
name="verify_student_midcourse_reverify"
),
......
......@@ -24,14 +24,15 @@ from django.contrib.auth.decorators import login_required
from course_modes.models import CourseMode
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.processors.CyberSource import (
get_signed_purchase_params, get_purchase_endpoint
)
from verify_student.models import (
SoftwareSecurePhotoVerification, MidcourseReverificationWindow,
SoftwareSecurePhotoVerification,
)
from reverification.models import MidcourseReverificationWindow
import ssencrypt
from xmodule.modulestore.exceptions import ItemNotFoundError
from .exceptions import WindowExpiredException
......@@ -48,7 +49,6 @@ class VerifyView(View):
- Taking the id photo
- Confirming that the photos and payment price are correct
before proceeding to payment
"""
upgrade = request.GET.get('upgrade', False)
......@@ -408,21 +408,35 @@ def midcourse_reverify_dash(_request):
except ItemNotFoundError:
log.error("User {0} enrolled in non-existent course {1}"
.format(user.username, enrollment.course_id))
reverify_course_data = []
reverifications_must_reverify = []
reverifications_denied = []
reverifications_pending = []
reverifications_approved = []
for (course, enrollment) in course_enrollment_pairs:
if MidcourseReverificationWindow.window_open_for_course(course.id):
reverify_course_data.append(
(
course.id,
course.display_name,
course.number,
MidcourseReverificationWindow.get_window(course.id, datetime.datetime.now(UTC)).end_date.strftime('%B %d, %Y %X %p'),
"must_reverify"
)
)
info = reverification_info(user, course, enrollment)
if info:
if "approved" in info:
reverifications_approved.append(info)
elif "pending" in info:
reverifications_pending.append(info)
elif "must_reverify" in info:
reverifications_must_reverify.append(info)
elif "denied" in info:
reverifications_denied.append(info)
# Sort the data by the reverification_end_date
reverifications_must_reverify = sorted(reverifications_must_reverify, key=lambda x: x[3])
reverifications_denied = sorted(reverifications_denied, key=lambda x: x[3])
reverifications_pending = sorted(reverifications_pending, key=lambda x: x[3])
reverifications_approved = sorted(reverifications_approved, key=lambda x: x[3])
context = {
"user_full_name": _request.user.profile.name,
"reverify_course_data": reverify_course_data,
"user_full_name": user.profile.name,
'reverifications_must_reverify': reverifications_must_reverify,
'reverifications_denied': reverifications_denied,
'reverifications_pending': reverifications_pending,
'reverifications_approved': reverifications_approved,
}
return render_to_response("verify_student/midcourse_reverify_dash.html", context)
......@@ -436,7 +450,7 @@ def reverification_submission_confirmation(_request):
@login_required
def midcourse_reverification_confirmation(_request):
def midcourse_reverification_confirmation(request): # pylint: disable=W0613
"""
Shows the user a confirmation page if the submission to SoftwareSecure was successful
"""
......
......@@ -1068,7 +1068,12 @@ INSTALLED_APPS = (
# Dark-launching languages
'dark_lang',
# Microsite configuration
'microsite_configuration',
# Student Identity Reverification
'reverification',
)
######################### MARKETING SITE ###############################
......
......@@ -181,6 +181,7 @@ ${fragment.foot_html()}
% endif
% if accordion:
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='courseware'" />
% endif
......
......@@ -11,6 +11,9 @@
<%static:css group='style-course'/>
</%block>
<%block name="title"><title>${_("{course.display_number_with_default} Course Info").format(course=course) | h}</title></%block>
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='info'" />
......
......@@ -26,8 +26,10 @@ ${progress_graph.body(grade_summary, course.grade_cutoffs, "grade-detail-graph",
</script>
</%block>
<%include file="/dashboard/_dashboard_prompt_midcourse_reverify.html" />
<%include file="/courseware/course_navigation.html" args="active_page='progress'" />
<section class="container">
<div class="profile-wrapper">
......
<%!
from django.core.urlresolvers import reverse
from xmodule.util.date_utils import get_time_display
from django.utils.translation import ugettext as _
%>
YOLO
%if status == "must_reverify":
You need to verify your ID again before DATE.
To continue in the verified track in this course, you need to re-verify by DATE. Why you need to verify yourself again.
%elif status == "denied":
You re-verification for this course failed and you are no longer eligible for a Verified Certificate. If you think this is in error, please contact us at support@edx.org
......@@ -152,9 +152,8 @@
</script>
</%block>
<!-- TODO later will need to make this ping for all courses on the dash -->
%if reverify_course_data:
% if reverifications_must_reverify or reverifications_denied:
<section class="dashboard-banner">
<div class="wrapper-msg">
<%include file='dashboard/_dashboard_prompt_midcourse_reverify.html' />
......@@ -199,6 +198,8 @@
<%include file='dashboard/_dashboard_status_verification.html' />
<%include file='dashboard/_dashboard_reverification_sidebar.html' />
</ul>
</section>
......
......@@ -2,11 +2,11 @@
<%! 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 reverify_course_data:
% if reverifications_must_reverify:
<div class="msg msg-reverify has-actions">
<div class="msg-content">
<h2 class="title">${_("You need to re-verify to continue")}</h2>
% for course_id, course_name, course_number, date, status in reverify_course_data:
% for course_id, course_name, course_number, date, status in reverifications_must_reverify:
<div class="copy">
<p class='activation-message'>
${_('To continue in the verified track in <strong>{course_name}</strong>, you need to re-verify your identity by {date}.').format(course_name=course_name, date=date)}
......@@ -23,3 +23,19 @@
</div>
% endfor
%endif
%if reverifications_denied:
<div class="msg msg-reverify has-actions">
<div class="msg-content">
<h2 class="title">${_("Your re-verification failed")}</h2>
% for course_id, course_name, course_number, date, status in reverifications_denied:
<div class="copy">
<p class='activation-message'>
${_('Your re-verification for <strong>{course_name}</strong> failed and you are no longer eligible for a Verified Certificate. If you think this is in error, please contact us at billing@edx.org.')}
</p>
</div>
</div>
</div>
</div>
% endfor
%endif
\ No newline at end of file
<%! 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_denied or reverifications_pending or reverifications_approved:
<h2 class="title">${_("Re-verification now open for")}</h2>
% if reverifications_must_reverify:
% for course_id, course_name, course_number, date, status in reverifications_must_reverify:
${_('Must Reverify: <strong>{course_name}</strong>').format(course_name=course_name)}
% endfor
%endif
% if reverifications_pending:
% for course_id, course_name, course_number, date, status in reverifications_pending:
${_('Denied: <strong>{course_name}</strong>').format(course_name=course_name)}
% endfor
%endif
% if reverifications_denied:
% for course_id, course_name, course_number, date, status in reverifications_denied:
${_('Denied: <strong>{course_name}</strong>').format(course_name=course_name)}
% endfor
%endif
% if reverifications_approved:
% for course_id, course_name, course_number, date, status in reverifications_approved:
${_('Denied: <strong>{course_name}</strong>').format(course_name=course_name)}
% endfor
%endif
%endif
\ No newline at end of file
......@@ -17,26 +17,40 @@
<h2 class="title">You are in the Verified track</h2>
<div class="copy">
% if len(reverifications_must_reverify) > 1:
<p>You currently need to re-verify for the following courses:</p>
% elif reverifications_must_reverify:
<p>You currently need to re-verify for the following course:</p>
% elif reverifications_pending or reverifications_approved or reverifications_denied:
<p>The status of your re-verifications is as follows:</p>
% else:
<p>You have no re-verifications at present.
% endif
<ul class="reverification-list">
% for course_id, course_name, course_number, date, status in reverify_course_data:
% for course_id, course_name, course_number, date, status in reverifications_must_reverify:
<li class="item">
<div class="course-info">
<h3 class="course-name">${course_name} (${course_number})</h3>
<p class="deadline">Re-verify by <strong>${date}</strong></p>
</div>
% if status == "must_reverify":
<p class="reverify-status"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': course_id})}">Re-verify for ${course_number}</a></p>
% elif status == "completed":
Completed
% elif status == "failed":
Failed
% endif
<p class="reverify-status"><a class="btn action-primary action-reverify" href="${reverse('verify_student_midcourse_reverify', kwargs={'course_id': course_id})}"
</li>
% endfor
</ul>
% for course_id, course_name, course_number, date, status in reverifications_pending:
<br/>Pending: ${course_name}
% endfor
% for course_id, course_name, course_number, date, status in reverifications_approved:
<br/>Approved: ${course_name}
% endfor
% for course_id, course_name, course_number, date, status in reverifications_denied:
<br/>Denied: ${course_name}
% endfor
</div>
<div class="wrapper-reverification-help list-faq">
......
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