Moved reverification windows into common

Added verification sidebar, banner for major courseware sections, quality & test improvements
This commit is contained in:
Julia Hansbrough
2014-01-24 19:33:07 +00:00
parent 003d82345f
commit 0b7594178b
30 changed files with 433 additions and 166 deletions

View File

@@ -451,9 +451,12 @@ INSTALLED_APPS = (
# for managing course modes
'course_modes',
<<<<<<< HEAD
# Dark-launching languages
'dark_lang',
# Student identity reverification
'reverification',
)

View File

@@ -0,0 +1,8 @@
"""
Reverification admin
"""
from ratelimitbackend import admin
from reverification.models import MidcourseReverificationWindow
admin.site.register(MidcourseReverificationWindow)

View File

@@ -0,0 +1,36 @@
# -*- 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']

View File

@@ -0,0 +1,54 @@
"""
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

View File

@@ -1,7 +1,7 @@
"""
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

View File

@@ -0,0 +1,72 @@
"""
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()

View File

@@ -1,3 +1,6 @@
"""
Utility functions for validating forms
"""
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordResetForm

View File

@@ -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,

View File

@@ -0,0 +1,14 @@
""" 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)

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,4 @@
from ratelimitbackend import admin
from verify_student.models import SoftwareSecurePhotoVerification
from verify_student.models import MidcourseReverificationWindow
admin.site.register(SoftwareSecurePhotoVerification)
admin.site.register(MidcourseReverificationWindow)

View File

@@ -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']

View 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

View File

@@ -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)
@@ -389,49 +407,6 @@ class TestPhotoVerification(TestCase):
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)

View File

@@ -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):

View File

@@ -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"
),

View File

@@ -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
"""

View File

@@ -1068,7 +1068,12 @@ INSTALLED_APPS = (
# Dark-launching languages
'dark_lang',
# Microsite configuration
'microsite_configuration',
# Student Identity Reverification
'reverification',
)
######################### MARKETING SITE ###############################

View File

@@ -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

View File

@@ -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'" />

View File

@@ -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">

View File

@@ -0,0 +1,14 @@
<%!
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

View File

@@ -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>

View File

@@ -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

View File

@@ -0,0 +1,32 @@
<%! 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

View 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">