Moved reverification windows into common
Added verification sidebar, banner for major courseware sections, quality & test improvements
This commit is contained in:
@@ -451,9 +451,12 @@ INSTALLED_APPS = (
|
||||
|
||||
# for managing course modes
|
||||
'course_modes',
|
||||
<<<<<<< HEAD
|
||||
|
||||
# Dark-launching languages
|
||||
'dark_lang',
|
||||
# Student identity reverification
|
||||
'reverification',
|
||||
)
|
||||
|
||||
|
||||
|
||||
0
common/djangoapps/reverification/__init__.py
Normal file
0
common/djangoapps/reverification/__init__.py
Normal file
8
common/djangoapps/reverification/admin.py
Normal file
8
common/djangoapps/reverification/admin.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Reverification admin
|
||||
"""
|
||||
|
||||
from ratelimitbackend import admin
|
||||
from reverification.models import MidcourseReverificationWindow
|
||||
|
||||
admin.site.register(MidcourseReverificationWindow)
|
||||
36
common/djangoapps/reverification/migrations/0001_initial.py
Normal file
36
common/djangoapps/reverification/migrations/0001_initial.py
Normal 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']
|
||||
54
common/djangoapps/reverification/models.py
Normal file
54
common/djangoapps/reverification/models.py
Normal 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
|
||||
0
common/djangoapps/reverification/tests/__init__.py
Normal file
0
common/djangoapps/reverification/tests/__init__.py
Normal 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
|
||||
72
common/djangoapps/reverification/tests/test_models.py
Normal file
72
common/djangoapps/reverification/tests/test_models.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
common/djangoapps/util/validate_on_save.py
Normal file
14
common/djangoapps/util/validate_on_save.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
14
lms/templates/courseware/reverify_banner.html
Normal file
14
lms/templates/courseware/reverify_banner.html
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user