Merge pull request #7530 from edx/aamir-khan/ECOM-912-in-course-reverification
Aamir khan/ecom 912 in course reverification
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from ratelimitbackend import admin
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
from verify_student.models import SoftwareSecurePhotoVerification, InCourseReverificationConfiguration
|
||||
|
||||
admin.site.register(SoftwareSecurePhotoVerification)
|
||||
admin.site.register(InCourseReverificationConfiguration)
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as 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 'VerificationCheckpoint'
|
||||
db.create_table('verify_student_verificationcheckpoint', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('course_id', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
('checkpoint_name', self.gf('django.db.models.fields.CharField')(max_length=32)),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['VerificationCheckpoint'])
|
||||
|
||||
# Adding M2M table for field photo_verification on 'VerificationCheckpoint'
|
||||
m2m_table_name = db.shorten_name('verify_student_verificationcheckpoint_photo_verification')
|
||||
db.create_table(m2m_table_name, (
|
||||
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('verificationcheckpoint', models.ForeignKey(orm['verify_student.verificationcheckpoint'], null=False)),
|
||||
('softwaresecurephotoverification', models.ForeignKey(orm['verify_student.softwaresecurephotoverification'], null=False))
|
||||
))
|
||||
db.create_unique(m2m_table_name, ['verificationcheckpoint_id', 'softwaresecurephotoverification_id'])
|
||||
|
||||
# Adding unique constraint on 'VerificationCheckpoint', fields ['course_id', 'checkpoint_name']
|
||||
db.create_unique('verify_student_verificationcheckpoint', ['course_id', 'checkpoint_name'])
|
||||
|
||||
# Adding model 'VerificationStatus'
|
||||
db.create_table('verify_student_verificationstatus', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('checkpoint', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['verify_student.VerificationCheckpoint'])),
|
||||
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
|
||||
('status', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
|
||||
('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('response', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
|
||||
('error', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['VerificationStatus'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Removing unique constraint on 'VerificationCheckpoint', fields ['course_id', 'checkpoint_name']
|
||||
db.delete_unique('verify_student_verificationcheckpoint', ['course_id', 'checkpoint_name'])
|
||||
|
||||
# Deleting model 'VerificationCheckpoint'
|
||||
db.delete_table('verify_student_verificationcheckpoint')
|
||||
|
||||
# Removing M2M table for field photo_verification on 'VerificationCheckpoint'
|
||||
db.delete_table(db.shorten_name('verify_student_verificationcheckpoint_photo_verification'))
|
||||
|
||||
# Deleting model 'VerificationStatus'
|
||||
db.delete_table('verify_student_verificationstatus')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reverification.midcoursereverificationwindow': {
|
||||
'Meta': {'object_name': 'MidcourseReverificationWindow'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'verify_student.softwaresecurephotoverification': {
|
||||
'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
|
||||
'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
|
||||
'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
|
||||
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'4f091843-1377-4d3b-af5d-3a4ae3d17943'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
|
||||
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
|
||||
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
|
||||
},
|
||||
'verify_student.verificationcheckpoint': {
|
||||
'Meta': {'unique_together': "(('course_id', 'checkpoint_name'),)", 'object_name': 'VerificationCheckpoint'},
|
||||
'checkpoint_name': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'photo_verification': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'symmetrical': 'False'})
|
||||
},
|
||||
'verify_student.verificationstatus': {
|
||||
'Meta': {'object_name': 'VerificationStatus'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.VerificationCheckpoint']"}),
|
||||
'error': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['verify_student']
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as 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 'InCourseReverificationConfiguration'
|
||||
db.create_table('verify_student_incoursereverificationconfiguration', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
|
||||
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['InCourseReverificationConfiguration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'InCourseReverificationConfiguration'
|
||||
db.delete_table('verify_student_incoursereverificationconfiguration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'reverification.midcoursereverificationwindow': {
|
||||
'Meta': {'object_name': 'MidcourseReverificationWindow'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'end_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
'verify_student.incoursereverificationconfiguration': {
|
||||
'Meta': {'object_name': 'InCourseReverificationConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
'verify_student.softwaresecurephotoverification': {
|
||||
'Meta': {'ordering': "['-created_at']", 'object_name': 'SoftwareSecurePhotoVerification'},
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'display': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
|
||||
'error_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
|
||||
'error_msg': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
'face_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_image_url': ('django.db.models.fields.URLField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'photo_id_key': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
|
||||
'receipt_id': ('django.db.models.fields.CharField', [], {'default': "'8a3c9d8a-b885-480e-8e1e-ca111326db42'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
|
||||
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
|
||||
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'window': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['reverification.MidcourseReverificationWindow']", 'null': 'True'})
|
||||
},
|
||||
'verify_student.verificationcheckpoint': {
|
||||
'Meta': {'unique_together': "(('course_id', 'checkpoint_name'),)", 'object_name': 'VerificationCheckpoint'},
|
||||
'checkpoint_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'photo_verification': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['verify_student.SoftwareSecurePhotoVerification']", 'symmetrical': 'False'})
|
||||
},
|
||||
'verify_student.verificationstatus': {
|
||||
'Meta': {'object_name': 'VerificationStatus'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['verify_student.VerificationCheckpoint']"}),
|
||||
'error': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'response': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'status': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
|
||||
'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['verify_student']
|
||||
|
||||
@@ -35,8 +35,11 @@ from verify_student.ssencrypt import (
|
||||
|
||||
from reverification.models import MidcourseReverificationWindow
|
||||
|
||||
from xmodule_django.models import CourseKeyField
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
def generateUUID(): # pylint: disable=invalid-name
|
||||
""" Utility function; generates UUIDs """
|
||||
@@ -640,6 +643,17 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
query = cls.objects.filter(user=user, window=None).order_by('-updated_at')
|
||||
return query[0]
|
||||
|
||||
@classmethod
|
||||
def get_initial_verification(cls, user):
|
||||
"""Get initial verification for a user
|
||||
Arguments:
|
||||
user(User): user object
|
||||
Return:
|
||||
SoftwareSecurePhotoVerification (object)
|
||||
"""
|
||||
init_verification = cls.objects.filter(user=user, status__in=["submitted", "approved"], window=None)
|
||||
return init_verification.latest('created_at') if init_verification.exists() else None
|
||||
|
||||
@status_before_must_be("created")
|
||||
def upload_face_image(self, img_data):
|
||||
"""
|
||||
@@ -881,3 +895,131 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
log.debug("Return message:\n\n{}\n\n".format(response.text))
|
||||
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def submit_faceimage(cls, user, face_image, photo_id_key):
|
||||
"""Submit the faceimage to SoftwareSecurePhotoVerification
|
||||
|
||||
Arguments:
|
||||
user(User): user object
|
||||
face_image (bytestream): raw bytestream image data
|
||||
photo_id_key (str) : SoftwareSecurePhotoVerification attribute
|
||||
Returns:
|
||||
SoftwareSecurePhotoVerification Object
|
||||
"""
|
||||
b64_face_image = face_image.split(",")[1]
|
||||
attempt = SoftwareSecurePhotoVerification(user=user)
|
||||
attempt.upload_face_image(b64_face_image.decode('base64'))
|
||||
attempt.photo_id_key = photo_id_key
|
||||
attempt.mark_ready()
|
||||
attempt.save()
|
||||
attempt.submit()
|
||||
return attempt
|
||||
|
||||
|
||||
class VerificationCheckpoint(models.Model):
|
||||
"""Represents a point at which a user is challenged to reverify his or her identity.
|
||||
Each checkpoint is uniquely identified by a (course_id, checkpoint_name) tuple.
|
||||
"""
|
||||
|
||||
CHECKPOINT_CHOICES = (
|
||||
("midterm", "midterm"),
|
||||
("final", "final"),
|
||||
)
|
||||
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
checkpoint_name = models.CharField(max_length=32, choices=CHECKPOINT_CHOICES)
|
||||
photo_verification = models.ManyToManyField(SoftwareSecurePhotoVerification)
|
||||
|
||||
class Meta: # pylint: disable=missing-docstring, old-style-class
|
||||
unique_together = (('course_id', 'checkpoint_name'),)
|
||||
|
||||
def add_verification_attempt(self, verification_attempt):
|
||||
""" Add the verification attempt in M2M relation of photo_verification
|
||||
|
||||
Arguments:
|
||||
verification_attempt(SoftwareSecurePhotoVerification): SoftwareSecurePhotoVerification object
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.photo_verification.add(verification_attempt) # pylint: disable=no-member
|
||||
|
||||
@classmethod
|
||||
def get_verification_checkpoint(cls, course_id, checkpoint_name):
|
||||
"""Get the verification checkpoint for given course_id and checkpoint name
|
||||
|
||||
Arguments:
|
||||
course_id(CourseKey): CourseKey
|
||||
checkpoint_name(str): checkpoint name
|
||||
|
||||
Returns:
|
||||
VerificationCheckpoint object if exists otherwise None
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(course_id=course_id, checkpoint_name=checkpoint_name)
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class VerificationStatus(models.Model):
|
||||
"""A verification status represents a user’s progress
|
||||
through the verification process for a particular checkpoint
|
||||
Model is an append-only table that represents the user status changes in
|
||||
verification process
|
||||
"""
|
||||
|
||||
VERIFICATION_STATUS_CHOICES = (
|
||||
("submitted", "submitted"),
|
||||
("approved", "approved"),
|
||||
("denied", "denied"),
|
||||
("error", "error")
|
||||
)
|
||||
|
||||
checkpoint = models.ForeignKey(VerificationCheckpoint)
|
||||
user = models.ForeignKey(User)
|
||||
status = models.CharField(choices=VERIFICATION_STATUS_CHOICES, db_index=True, max_length=32)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
response = models.TextField(null=True, blank=True)
|
||||
error = models.TextField(null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def add_verification_status(cls, checkpoint, user, status):
|
||||
""" Create new verification status object
|
||||
|
||||
Arguments:
|
||||
checkpoint(VerificationCheckpoint): VerificationCheckpoint object
|
||||
user(User): user object
|
||||
status(str): String representing the status from VERIFICATION_STATUS_CHOICES
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
cls.objects.create(checkpoint=checkpoint, user=user, status=status)
|
||||
|
||||
@classmethod
|
||||
def add_status_from_checkpoints(cls, checkpoints, user, status):
|
||||
""" Create new verification status objects against the given checkpoints
|
||||
|
||||
Arguments:
|
||||
checkpoints(list): list of VerificationCheckpoint objects
|
||||
user(User): user object
|
||||
status(str): String representing the status from VERIFICATION_STATUS_CHOICES
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
for checkpoint in checkpoints:
|
||||
cls.objects.create(checkpoint=checkpoint, user=user, status=status)
|
||||
|
||||
|
||||
class InCourseReverificationConfiguration(ConfigurationModel):
|
||||
"""Configure in-course re-verification.
|
||||
|
||||
Enable or disable in-course re-verification feature.
|
||||
When this flag is disabled, the "in-course re-verification" feature
|
||||
will be disabled.
|
||||
|
||||
When the flag is enabled, the "in-course re-verification" feature
|
||||
will be enabled.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import timedelta, datetime
|
||||
import ddt
|
||||
import json
|
||||
import requests.exceptions
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from mock import patch
|
||||
from nose.tools import assert_is_none, assert_equals, assert_raises, assert_true, assert_false # pylint: disable=E0611
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
@@ -16,7 +18,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification, VerificationException,
|
||||
SoftwareSecurePhotoVerification, VerificationException, VerificationCheckpoint, VerificationStatus
|
||||
)
|
||||
|
||||
FAKE_SETTINGS = {
|
||||
@@ -599,3 +601,111 @@ class TestMidcourseReverification(ModuleStoreTestCase):
|
||||
attempt.status = "approved"
|
||||
attempt.save()
|
||||
assert_true(SoftwareSecurePhotoVerification.user_has_valid_or_pending(user=self.user, window=window))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class VerificationCheckpointTest(ModuleStoreTestCase):
|
||||
"""Tests for the VerificationCheckpoint model. """
|
||||
|
||||
MIDTERM = "midterm"
|
||||
FINAL = "final"
|
||||
|
||||
def setUp(self):
|
||||
super(VerificationCheckpointTest, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
@ddt.data(MIDTERM, FINAL)
|
||||
def test_get_verification_checkpoint(self, check_point):
|
||||
"""testing class method of VerificationCheckpoint. create the object and then uses the class method to get the
|
||||
verification check point.
|
||||
"""
|
||||
# create the VerificationCheckpoint checkpoint
|
||||
ver_check_point = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=check_point)
|
||||
self.assertEqual(
|
||||
VerificationCheckpoint.get_verification_checkpoint(self.course.id, check_point),
|
||||
ver_check_point
|
||||
)
|
||||
|
||||
def test_get_verification_checkpoint_for_not_existing_values(self):
|
||||
"""testing class method of VerificationCheckpoint. create the object and then uses the class method to get the
|
||||
verification check point.
|
||||
"""
|
||||
# create the VerificationCheckpoint checkpoint
|
||||
VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=self.MIDTERM)
|
||||
|
||||
# get verification for not existing checkpoint
|
||||
self.assertEqual(VerificationCheckpoint.get_verification_checkpoint(self.course.id, 'abc'), None)
|
||||
|
||||
def test_unique_together_constraint(self):
|
||||
"""testing the unique together contraint.
|
||||
"""
|
||||
# create the VerificationCheckpoint checkpoint
|
||||
VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=self.MIDTERM)
|
||||
|
||||
# create the VerificationCheckpoint checkpoint with same course id and checkpoint name
|
||||
with self.assertRaises(IntegrityError):
|
||||
VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=self.MIDTERM)
|
||||
|
||||
def test_add_verification_attempt_software_secure(self):
|
||||
"""testing manytomany relationship. adding softwaresecure attempt to the verification checkpoints.
|
||||
"""
|
||||
# adding two check points.
|
||||
check_point1 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=self.MIDTERM)
|
||||
check_point2 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name=self.FINAL)
|
||||
|
||||
# Make an attempt and added to the checkpoint1.
|
||||
check_point1.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user))
|
||||
self.assertEqual(check_point1.photo_verification.count(), 1)
|
||||
|
||||
# Make an other attempt and added to the checkpoint1.
|
||||
check_point1.add_verification_attempt(SoftwareSecurePhotoVerification.objects.create(user=self.user))
|
||||
self.assertEqual(check_point1.photo_verification.count(), 2)
|
||||
|
||||
# make new attempt and adding to the checkpoint2
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=self.user)
|
||||
check_point2.add_verification_attempt(attempt)
|
||||
self.assertEqual(check_point2.photo_verification.count(), 1)
|
||||
|
||||
# remove the attempt from checkpoint2
|
||||
check_point2.photo_verification.remove(attempt)
|
||||
self.assertEqual(check_point2.photo_verification.count(), 0)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class VerificationStatusTest(ModuleStoreTestCase):
|
||||
"""Tests for the VerificationStatus model. """
|
||||
|
||||
def setUp(self):
|
||||
super(VerificationStatusTest, self).setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.course = CourseFactory.create()
|
||||
self.check_point1 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name="midterm")
|
||||
self.check_point2 = VerificationCheckpoint.objects.create(course_id=self.course.id, checkpoint_name="final")
|
||||
|
||||
@ddt.data('submitted', "approved", "denied", "error")
|
||||
def test_add_verification_status(self, status):
|
||||
"""adding verfication status using the class method."""
|
||||
|
||||
# adding verification status
|
||||
VerificationStatus.add_verification_status(checkpoint=self.check_point1, user=self.user, status=status)
|
||||
|
||||
# getting the status from db
|
||||
result = VerificationStatus.objects.filter(checkpoint=self.check_point1)[0]
|
||||
self.assertEqual(result.status, status)
|
||||
self.assertEqual(result.user, self.user)
|
||||
|
||||
@ddt.data('submitted', "approved", "denied", "error")
|
||||
def test_add_status_from_checkpoints(self, status):
|
||||
"""adding verfication status for checkpoints list."""
|
||||
|
||||
# adding verification status with multiple points
|
||||
VerificationStatus.add_status_from_checkpoints(
|
||||
checkpoints=[self.check_point1, self.check_point2], user=self.user, status=status
|
||||
)
|
||||
|
||||
# getting the status from db.
|
||||
result = VerificationStatus.objects.filter(user=self.user)
|
||||
self.assertEqual(len(result), len([self.check_point1.checkpoint_name, self.check_point2.checkpoint_name]))
|
||||
self.assertEqual(result[0].checkpoint.checkpoint_name, self.check_point1.checkpoint_name)
|
||||
self.assertEqual(result[1].checkpoint.checkpoint_name, self.check_point2.checkpoint_name)
|
||||
|
||||
@@ -36,8 +36,14 @@ from course_modes.models import CourseMode
|
||||
from shoppingcart.models import Order, CertificateItem
|
||||
from embargo.test_utils import restrict_course
|
||||
from util.testing import UrlResetMixin
|
||||
from verify_student.views import render_to_response, PayAndVerifyView
|
||||
from verify_student.models import SoftwareSecurePhotoVerification
|
||||
from verify_student.views import (
|
||||
render_to_response, PayAndVerifyView, EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW,
|
||||
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY
|
||||
)
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification, VerificationCheckpoint,
|
||||
InCourseReverificationConfiguration
|
||||
)
|
||||
from reverification.tests.factories import MidcourseReverificationWindowFactory
|
||||
|
||||
|
||||
@@ -1686,3 +1692,175 @@ class TestReverificationBanner(ModuleStoreTestCase):
|
||||
self.client.post(reverse('verify_student_toggle_failed_banner_off'))
|
||||
photo_verification = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=self.window)
|
||||
self.assertFalse(photo_verification.display)
|
||||
|
||||
|
||||
class TestInCourseReverifyView(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the incourse reverification views.
|
||||
"""
|
||||
IMAGE_DATA = "abcd,1234"
|
||||
MIDTERM = "midterm"
|
||||
|
||||
def setUp(self):
|
||||
super(TestInCourseReverifyView, self).setUp()
|
||||
|
||||
self.user = UserFactory.create(username="rusty", password="test")
|
||||
self.client.login(username="rusty", password="test")
|
||||
|
||||
self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course")
|
||||
CourseFactory.create(org='Robot', number='999', display_name='Test Course')
|
||||
|
||||
# Create the course modes
|
||||
for mode in ('audit', 'honor', 'verified'):
|
||||
min_price = 0 if mode in ["honor", "audit"] else 1
|
||||
CourseModeFactory(mode_slug=mode, course_id=self.course_key, min_price=min_price)
|
||||
|
||||
# Enroll the user in the default mode (honor) to emulate
|
||||
CourseEnrollment.enroll(self.user, self.course_key, mode="verified")
|
||||
self.config = InCourseReverificationConfiguration(enabled=True)
|
||||
self.config.save()
|
||||
|
||||
# mocking and patching for bi events
|
||||
analytics_patcher = patch('verify_student.views.analytics')
|
||||
self.mock_tracker = analytics_patcher.start()
|
||||
self.addCleanup(analytics_patcher.stop)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_feature_flag_get(self):
|
||||
self.config.enabled = False
|
||||
self.config.save()
|
||||
response = self.client.get(self._get_url(self.course_key, self.MIDTERM))
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_invalid_course_get(self):
|
||||
response = self.client.get(self._get_url("invalid/course/key", self.MIDTERM))
|
||||
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_invalid_checkpoint_get(self):
|
||||
response = self.client.get(self._get_url(self.course_key, "invalid_checkpoint"))
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_initial_redirect_get(self):
|
||||
self._create_checkpoint()
|
||||
response = self.client.get(self._get_url(self.course_key, self.MIDTERM))
|
||||
|
||||
url = reverse('verify_student_verify_later',
|
||||
kwargs={"course_id": unicode(self.course_key)})
|
||||
self.assertRedirects(response, url)
|
||||
|
||||
@override_settings(SEGMENT_IO_LMS_KEY="foobar")
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True, 'SEGMENT_IO_LMS': True})
|
||||
def test_incourse_reverify_get(self):
|
||||
self._create_checkpoint()
|
||||
self._create_initial_verification()
|
||||
|
||||
response = self.client.get(self._get_url(self.course_key, self.MIDTERM))
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
#Verify Google Analytics event fired after successfully submiting the picture
|
||||
self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member
|
||||
self.user.id, # pylint: disable=no-member
|
||||
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW,
|
||||
{
|
||||
'category': "verification",
|
||||
'label': unicode(self.course_key),
|
||||
'checkpoint': self.MIDTERM
|
||||
},
|
||||
|
||||
context={
|
||||
'Google Analytics':
|
||||
{'clientId': None}
|
||||
}
|
||||
)
|
||||
self.mock_tracker.reset_mock()
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
@patch('verify_student.views.render_to_response', render_mock)
|
||||
def test_invalid_checkpoint_post(self):
|
||||
|
||||
response = self.client.post(self._get_url(self.course_key, self.MIDTERM))
|
||||
self.assertEquals(response.status_code, 200)
|
||||
((template, context), _kwargs) = render_mock.call_args # pylint: disable=unpacking-non-sequence
|
||||
self.assertIn('incourse_reverify', template)
|
||||
self.assertTrue(context['error'])
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_initial_redirect_post(self):
|
||||
self._create_checkpoint()
|
||||
|
||||
response = self.client.post(self._get_url(self.course_key, self.MIDTERM))
|
||||
url = reverse('verify_student_verify_later',
|
||||
kwargs={"course_id": unicode(self.course_key)})
|
||||
|
||||
self.assertRedirects(response, url)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_index_error_post(self):
|
||||
self._create_checkpoint()
|
||||
self._create_initial_verification()
|
||||
|
||||
response = self.client.post(self._get_url(self.course_key, self.MIDTERM), {"face_image": ""})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
@override_settings(SEGMENT_IO_LMS_KEY="foobar")
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True, 'SEGMENT_IO_LMS': True})
|
||||
def test_incourse_reverify_post(self):
|
||||
self._create_checkpoint()
|
||||
self._create_initial_verification()
|
||||
|
||||
response = self.client.post(self._get_url(self.course_key, self.MIDTERM), {"face_image": self.IMAGE_DATA})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
#Verify Google Analytics event fired after successfully submiting the picture
|
||||
self.mock_tracker.track.assert_called_once_with( # pylint: disable=no-member
|
||||
self.user.id, # pylint: disable=no-member
|
||||
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY,
|
||||
{
|
||||
'category': "verification",
|
||||
'label': unicode(self.course_key),
|
||||
'checkpoint': self.MIDTERM
|
||||
},
|
||||
|
||||
context={
|
||||
'Google Analytics':
|
||||
{'clientId': None}
|
||||
}
|
||||
)
|
||||
self.mock_tracker.reset_mock()
|
||||
|
||||
@patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True})
|
||||
def test_incourse_reverify_feature_flag_post(self):
|
||||
self.config.enabled = False
|
||||
self.config.save()
|
||||
|
||||
response = self.client.post(self._get_url(self.course_key, self.MIDTERM))
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
def _create_checkpoint(self):
|
||||
"""helper method for creating checkpoint"""
|
||||
checkpoint = VerificationCheckpoint(course_id=self.course_key, checkpoint_name=self.MIDTERM)
|
||||
checkpoint.save()
|
||||
|
||||
def _create_initial_verification(self):
|
||||
"""helper method for initial verification"""
|
||||
attempt = SoftwareSecurePhotoVerification(user=self.user)
|
||||
attempt.mark_ready()
|
||||
attempt.save()
|
||||
attempt.submit()
|
||||
|
||||
def _get_url(self, course_key, checkpoint):
|
||||
"""contruct the url.
|
||||
|
||||
Arguments:
|
||||
course_key (unicode): The ID of the course.
|
||||
checkpoint (str): The verification checkpoint
|
||||
Returns:
|
||||
url
|
||||
|
||||
"""
|
||||
return reverse('verify_student_incourse_reverify',
|
||||
kwargs={"course_id": unicode(course_key), "checkpoint_name": checkpoint})
|
||||
|
||||
@@ -139,4 +139,14 @@ urlpatterns = patterns(
|
||||
views.submit_photos_for_verification,
|
||||
name="verify_student_submit_photos"
|
||||
),
|
||||
# Endpoint for in-course reverification
|
||||
# Users are sent to this end-point from within courseware
|
||||
# to re-verify their identities by re-submitting face photos.
|
||||
url(
|
||||
r'^reverify/{course_id}/{checkpoint}/$'.format(
|
||||
course_id=settings.COURSE_ID_PATTERN, checkpoint=settings.CHECKPOINT_PATTERN
|
||||
),
|
||||
views.InCourseReverifyView.as_view(),
|
||||
name="verify_student_incourse_reverify"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -43,7 +43,9 @@ from shoppingcart.processors import (
|
||||
)
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification,
|
||||
)
|
||||
VerificationCheckpoint,
|
||||
VerificationStatus,
|
||||
InCourseReverificationConfiguration)
|
||||
from reverification.models import MidcourseReverificationWindow
|
||||
import ssencrypt
|
||||
from .exceptions import WindowExpiredException
|
||||
@@ -51,6 +53,8 @@ from microsite_configuration import microsite
|
||||
from embargo import api as embargo_api
|
||||
from util.json_request import JsonResponse
|
||||
from util.date_utils import get_default_time_display
|
||||
from eventtracking import tracker
|
||||
import analytics
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -59,6 +63,9 @@ EVENT_NAME_USER_ENTERED_MIDCOURSE_REVERIFY_VIEW = 'edx.course.enrollment.reverif
|
||||
EVENT_NAME_USER_SUBMITTED_MIDCOURSE_REVERIFY = 'edx.course.enrollment.reverify.submitted'
|
||||
EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE = 'edx.course.enrollment.reverify.reviewed'
|
||||
|
||||
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW = 'edx.bi.reverify.started'
|
||||
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY = 'edx.bi.reverify.submitted'
|
||||
|
||||
|
||||
class PayAndVerifyView(View):
|
||||
"""View for the "verify and pay" flow.
|
||||
@@ -845,15 +852,20 @@ def results_callback(request):
|
||||
log.error("Software Secure posted back for receipt_id {}, but not found".format(receipt_id))
|
||||
return HttpResponseBadRequest("edX ID {} not found".format(receipt_id))
|
||||
|
||||
checkpoints = VerificationCheckpoint.objects.filter(photo_verification=attempt).all()
|
||||
|
||||
if result == "PASS":
|
||||
log.debug("Approving verification for {}".format(receipt_id))
|
||||
attempt.approve()
|
||||
status = "approved"
|
||||
elif result == "FAIL":
|
||||
log.debug("Denying verification for {}".format(receipt_id))
|
||||
attempt.deny(json.dumps(reason), error_code=error_code)
|
||||
status = "denied"
|
||||
elif result == "SYSTEM FAIL":
|
||||
log.debug("System failure for {} -- resetting to must_retry".format(receipt_id))
|
||||
attempt.system_error(json.dumps(reason), error_code=error_code)
|
||||
status = "error"
|
||||
log.error("Software Secure callback attempt for %s failed: %s", receipt_id, reason)
|
||||
else:
|
||||
log.error("Software Secure returned unknown result {}".format(result))
|
||||
@@ -866,7 +878,7 @@ def results_callback(request):
|
||||
course_id = attempt.window.course_id
|
||||
course_enrollment = CourseEnrollment.get_or_create_enrollment(attempt.user, course_id)
|
||||
course_enrollment.emit_event(EVENT_NAME_USER_REVERIFICATION_REVIEWED_BY_SOFTWARESECURE)
|
||||
|
||||
VerificationStatus.add_status_from_checkpoints(checkpoints=checkpoints, user=attempt.user, status=status)
|
||||
return HttpResponse("OK!")
|
||||
|
||||
|
||||
@@ -1064,3 +1076,138 @@ def reverification_window_expired(_request):
|
||||
"""
|
||||
# TODO need someone to review the copy for this template
|
||||
return render_to_response("verify_student/reverification_window_expired.html")
|
||||
|
||||
|
||||
class InCourseReverifyView(View):
|
||||
"""
|
||||
The in-course reverification view.
|
||||
Needs to perform these functions:
|
||||
- take new face photo
|
||||
- retrieve the old id photo
|
||||
- submit these photos to photo verification service
|
||||
|
||||
Does not need to worry about pricing
|
||||
"""
|
||||
@method_decorator(login_required)
|
||||
def get(self, request, course_id, checkpoint_name):
|
||||
""" Display the view for face photo submission"""
|
||||
# Check the in-course re-verification is enabled or not
|
||||
incourse_reverify_enabled = InCourseReverificationConfiguration.current().enabled
|
||||
if not incourse_reverify_enabled:
|
||||
raise Http404
|
||||
|
||||
user = request.user
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = modulestore().get_course(course_key)
|
||||
if course is None:
|
||||
raise Http404
|
||||
|
||||
checkpoint = VerificationCheckpoint.get_verification_checkpoint(course_key, checkpoint_name)
|
||||
if checkpoint is None:
|
||||
raise Http404
|
||||
|
||||
init_verification = SoftwareSecurePhotoVerification.get_initial_verification(user)
|
||||
if not init_verification:
|
||||
return redirect(reverse('verify_student_verify_later', kwargs={'course_id': unicode(course_key)}))
|
||||
|
||||
# emit the reverification event
|
||||
self._track_reverification_events(
|
||||
EVENT_NAME_USER_ENTERED_INCOURSE_REVERIFY_VIEW, user.id, course_id, checkpoint_name
|
||||
)
|
||||
|
||||
context = {
|
||||
'course_key': unicode(course_key),
|
||||
'course_name': course.display_name_with_default,
|
||||
'checkpoint_name': checkpoint_name,
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
}
|
||||
return render_to_response("verify_student/incourse_reverify.html", context)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, course_id, checkpoint_name):
|
||||
"""Submits the re-verification attempt to SoftwareSecure
|
||||
|
||||
Args:
|
||||
request(HttpRequest): HttpRequest object
|
||||
course_id(str): Course Id
|
||||
checkpoint_name(str): Checkpoint name
|
||||
|
||||
Returns:
|
||||
HttpResponse with status_code 400 if photo is missing or any error
|
||||
or redirect to verify_student_verify_later url if initial verification doesn't exist otherwise
|
||||
HttpsResponse with status code 200
|
||||
"""
|
||||
# Check the in-course re-verification is enabled or not
|
||||
incourse_reverify_enabled = InCourseReverificationConfiguration.current().enabled
|
||||
if not incourse_reverify_enabled:
|
||||
raise Http404
|
||||
|
||||
user = request.user
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = modulestore().get_course(course_key)
|
||||
checkpoint = VerificationCheckpoint.get_verification_checkpoint(course_key, checkpoint_name)
|
||||
if checkpoint is None:
|
||||
log.error("Checkpoint is not defined. Could not submit verification attempt for user %s",
|
||||
request.user.id)
|
||||
context = {
|
||||
'course_key': unicode(course_key),
|
||||
'course_name': course.display_name_with_default,
|
||||
'checkpoint_name': checkpoint_name,
|
||||
'error': True,
|
||||
'errorMsg': _("No checkpoint found"),
|
||||
'platform_name': settings.PLATFORM_NAME,
|
||||
}
|
||||
return render_to_response("verify_student/incourse_reverify.html", context)
|
||||
init_verification = SoftwareSecurePhotoVerification.get_initial_verification(user)
|
||||
if not init_verification:
|
||||
log.error("Could not submit verification attempt for user %s", request.user.id)
|
||||
return redirect(reverse('verify_student_verify_later', kwargs={'course_id': unicode(course_key)}))
|
||||
|
||||
try:
|
||||
attempt = SoftwareSecurePhotoVerification.submit_faceimage(
|
||||
request.user, request.POST['face_image'], init_verification.photo_id_key
|
||||
)
|
||||
checkpoint.add_verification_attempt(attempt)
|
||||
VerificationStatus.add_verification_status(checkpoint, user, "submitted")
|
||||
|
||||
# emit the reverification event
|
||||
self._track_reverification_events(
|
||||
EVENT_NAME_USER_SUBMITTED_INCOURSE_REVERIFY, user.id, course_id, checkpoint_name
|
||||
)
|
||||
|
||||
return HttpResponse()
|
||||
except IndexError:
|
||||
log.exception("Invalid image data during photo verification.")
|
||||
return HttpResponseBadRequest(_("Invalid image data during photo verification."))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.exception("Could not submit verification attempt for user {}.").format(request.user.id)
|
||||
msg = _("Could not submit photos")
|
||||
return HttpResponseBadRequest(msg)
|
||||
|
||||
def _track_reverification_events(self, event_name, user_id, course_id, checkpoint): # pylint: disable=invalid-name
|
||||
"""Track re-verification events for user against course checkpoints
|
||||
|
||||
Arguments:
|
||||
user_id (str): The ID of the user generting the certificate.
|
||||
course_id (unicode): id associated with the course
|
||||
checkpoint (str): checkpoint name
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
if settings.FEATURES.get('SEGMENT_IO_LMS') and hasattr(settings, 'SEGMENT_IO_LMS_KEY'):
|
||||
tracking_context = tracker.get_tracker().resolve_context()
|
||||
analytics.track(
|
||||
user_id,
|
||||
event_name,
|
||||
{
|
||||
'category': "verification",
|
||||
'label': unicode(course_id),
|
||||
'checkpoint': checkpoint
|
||||
},
|
||||
context={
|
||||
'Google Analytics': {
|
||||
'clientId': tracking_context.get('client_id')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1180,6 +1180,15 @@ verify_student_js = [
|
||||
'js/verify_student/pay_and_verify.js',
|
||||
]
|
||||
|
||||
reverify_js = [
|
||||
'js/verify_student/views/error_view.js',
|
||||
'js/verify_student/views/image_input_view.js',
|
||||
'js/verify_student/views/webcam_photo_view.js',
|
||||
'js/verify_student/models/reverification_model.js',
|
||||
'js/verify_student/views/incourse_reverify_view.js',
|
||||
'js/verify_student/incourse_reverify.js',
|
||||
]
|
||||
|
||||
PIPELINE_CSS = {
|
||||
'style-vendor': {
|
||||
'source_filenames': [
|
||||
@@ -1369,6 +1378,10 @@ PIPELINE_JS = {
|
||||
'verify_student': {
|
||||
'source_filenames': verify_student_js,
|
||||
'output_filename': 'js/verify_student.js'
|
||||
},
|
||||
'reverify': {
|
||||
'source_filenames': reverify_js,
|
||||
'output_filename': 'js/reverify.js'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2193,3 +2206,6 @@ ACCOUNT_VISIBILITY_CONFIGURATION = {
|
||||
ECOMMERCE_API_URL = None
|
||||
ECOMMERCE_API_SIGNING_KEY = None
|
||||
ECOMMERCE_API_TIMEOUT = 5
|
||||
|
||||
# Reverification checkpoint name pattern
|
||||
CHECKPOINT_PATTERN = r'(?P<checkpoint_name>\w+)'
|
||||
|
||||
28
lms/static/js/verify_student/incourse_reverify.js
Normal file
28
lms/static/js/verify_student/incourse_reverify.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Set up the in-course reverification page.
|
||||
*
|
||||
* This loads data from the DOM's "data-*" attributes
|
||||
* and uses these to initialize the top-level views
|
||||
* on the page.
|
||||
*/
|
||||
var edx = edx || {};
|
||||
|
||||
(function( $, _ ) {
|
||||
'use strict';
|
||||
var errorView,
|
||||
el = $('#incourse-reverify-container');
|
||||
|
||||
edx.verify_student = edx.verify_student || {};
|
||||
|
||||
errorView = new edx.verify_student.ErrorView({
|
||||
el: $('#error-container')
|
||||
});
|
||||
|
||||
return new edx.verify_student.InCourseReverifyView({
|
||||
courseKey: el.data('course-key'),
|
||||
checkpointName: el.data('checkpoint-name'),
|
||||
platformName: el.data('platform-name'),
|
||||
errorModel: errorView.model
|
||||
}).render();
|
||||
|
||||
})( jQuery, _ );
|
||||
51
lms/static/js/verify_student/models/reverification_model.js
Normal file
51
lms/static/js/verify_student/models/reverification_model.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Model for a reverification attempt.
|
||||
*
|
||||
* The re-verification model is responsible for
|
||||
* storing face photo image data and submitting
|
||||
* it back to the server.
|
||||
*/
|
||||
var edx = edx || {};
|
||||
|
||||
(function( $, _, Backbone ) {
|
||||
'use strict';
|
||||
|
||||
edx.verify_student = edx.verify_student || {};
|
||||
|
||||
edx.verify_student.ReverificationModel = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
courseKey: '',
|
||||
checkpointName: '',
|
||||
faceImage: '',
|
||||
},
|
||||
|
||||
sync: function( method ) {
|
||||
var model = this;
|
||||
var headers = { 'X-CSRFToken': $.cookie( 'csrftoken' ) },
|
||||
data = {
|
||||
face_image: model.get( 'faceImage' ),
|
||||
},
|
||||
url = _.str.sprintf(
|
||||
'/verify_student/reverify/%(courseKey)s/%(checkpointName)s/', {
|
||||
courseKey: model.get('courseKey'),
|
||||
checkpointName: model.get('checkpointName')
|
||||
}
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
headers: headers,
|
||||
success: function() {
|
||||
model.trigger( 'sync' );
|
||||
},
|
||||
error: function( error ) {
|
||||
model.trigger( 'error', error );
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})( jQuery, _, Backbone );
|
||||
107
lms/static/js/verify_student/views/incourse_reverify_view.js
Normal file
107
lms/static/js/verify_student/views/incourse_reverify_view.js
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* View for in-course reverification.
|
||||
*
|
||||
* This view is responsible for rendering the page
|
||||
* template, including any subviews (for photo capture).
|
||||
*/
|
||||
var edx = edx || {};
|
||||
|
||||
(function( $, _, _s, Backbone, gettext ) {
|
||||
'use strict';
|
||||
|
||||
edx.verify_student = edx.verify_student || {};
|
||||
|
||||
edx.verify_student.InCourseReverifyView = Backbone.View.extend({
|
||||
|
||||
el: '#incourse-reverify-container',
|
||||
templateId: '#incourse_reverify-tpl',
|
||||
submitButtonId: '#submit',
|
||||
|
||||
events: {
|
||||
'click #submit': 'submitPhoto'
|
||||
},
|
||||
|
||||
initialize: function( obj ) {
|
||||
_.mixin( _s.exports() );
|
||||
|
||||
this.errorModel = obj.errorModel || null;
|
||||
this.courseKey = obj.courseKey || null;
|
||||
this.checkpointName = obj.checkpointName || null;
|
||||
this.platformName = obj.platformName || null;
|
||||
|
||||
|
||||
this.model = new edx.verify_student.ReverificationModel({
|
||||
courseKey: this.courseKey,
|
||||
checkpointName: this.checkpointName
|
||||
});
|
||||
|
||||
this.listenTo( this.model, 'sync', _.bind( this.handleSubmitPhotoSuccess, this ));
|
||||
this.listenTo( this.model, 'error', _.bind( this.handleSubmissionError, this ));
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var renderedTemplate = _.template(
|
||||
$( this.templateId ).html(),
|
||||
{
|
||||
courseKey: this.courseKey,
|
||||
checkpointName: this.checkpointName,
|
||||
platformName: this.platformName
|
||||
}
|
||||
);
|
||||
$( this.el ).html( renderedTemplate );
|
||||
|
||||
// Render the webcam view *after* the parent view
|
||||
// so that the container div for the webcam
|
||||
// exists in the DOM.
|
||||
this.renderWebcam();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
renderWebcam: function() {
|
||||
edx.verify_student.getSupportedWebcamView({
|
||||
el: $( '#webcam' ),
|
||||
model: this.model,
|
||||
modelAttribute: 'faceImage',
|
||||
submitButton: this.submitButtonId,
|
||||
errorModel: this.errorModel
|
||||
}).render();
|
||||
},
|
||||
|
||||
submitPhoto: function() {
|
||||
// disable the submit button to prevent multiple submissions.
|
||||
this.setSubmitButtonEnabled(false)
|
||||
this.model.save();
|
||||
},
|
||||
|
||||
handleSubmitPhotoSuccess: function() {
|
||||
// Eventually this will be a redirect back into the courseware,
|
||||
// but for now we can return to the student dashboard.
|
||||
window.location.href = '/dashboard';
|
||||
},
|
||||
|
||||
handleSubmissionError: function(xhr) {
|
||||
var errorMsg = gettext( 'An error has occurred. Please try again later.' );
|
||||
|
||||
// Re-enable the submit button to allow the user to retry
|
||||
this.setSubmitButtonEnabled( true );
|
||||
|
||||
if ( xhr.status === 400 ) {
|
||||
errorMsg = xhr.responseText;
|
||||
}
|
||||
|
||||
this.errorModel.set({
|
||||
errorTitle: gettext( 'Could not submit photos' ),
|
||||
errorMsg: errorMsg,
|
||||
shown: true
|
||||
});
|
||||
},
|
||||
setSubmitButtonEnabled: function( isEnabled ) {
|
||||
$(this.submitButtonId)
|
||||
.toggleClass( 'is-disabled', !isEnabled )
|
||||
.prop( 'disabled', !isEnabled )
|
||||
.attr('aria-disabled', !isEnabled);
|
||||
}
|
||||
});
|
||||
})(jQuery, _, _.str, Backbone, gettext);
|
||||
@@ -1,6 +1,6 @@
|
||||
// Updates for decoupled verification A/B test
|
||||
.verification-process {
|
||||
.pay-and-verify {
|
||||
.pay-and-verify, .incourse-reverify {
|
||||
.review {
|
||||
.title.center-col {
|
||||
padding: 0 calc( ( 100% - 750px ) / 2 ) 10px;
|
||||
|
||||
52
lms/templates/verify_student/incourse_reverify.html
Normal file
52
lms/templates/verify_student/incourse_reverify.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<%!
|
||||
import json
|
||||
from django.utils.translation import ugettext as _
|
||||
from verify_student.views import PayAndVerifyView
|
||||
%>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
<%block name="bodyclass">register verification-process step-requirements</%block>
|
||||
|
||||
<%block name="pagetitle">
|
||||
${_("Verifying for \"{checkpoint_name}\" in course \"{course_name}\"").format(course_name=course_name,
|
||||
checkpoint_name=checkpoint_name)}
|
||||
</%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
% for template_name in ["incourse_reverify", "webcam_photo", "image_input", "error"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="verify_student/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
</%block>
|
||||
<%block name="js_extra">
|
||||
<%static:js group='rwd_header_footer'/>
|
||||
<script src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
|
||||
<script src="${static.url('js/vendor/backbone-min.js')}"></script>
|
||||
<script src="${static.url('js/src/tooltip_manager.js')}"></script>
|
||||
<%static:js group='reverify'/>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
## Top-level wrapper for errors
|
||||
## JavaScript views may append to this wrapper
|
||||
<div id="error-container" style="display: none;"></div>
|
||||
|
||||
<div class="container">
|
||||
<section class="wrapper carousel">
|
||||
## Container for the reverification view.
|
||||
## The Backbone view renders itself into this <div>.
|
||||
## The server can pass information to the Backbone view
|
||||
## by including the information as "data-*" attributes
|
||||
## of this </div>.
|
||||
<div id="incourse-reverify-container"
|
||||
class="incourse-reverify"
|
||||
data-course-key='${course_key}'
|
||||
data-checkpoint-name='${checkpoint_name}'
|
||||
data-platform-name='${platform_name}'
|
||||
></div>
|
||||
</section>
|
||||
</div>
|
||||
</%block>
|
||||
55
lms/templates/verify_student/incourse_reverify.underscore
Normal file
55
lms/templates/verify_student/incourse_reverify.underscore
Normal file
@@ -0,0 +1,55 @@
|
||||
<div id="wrapper-facephoto" class="wrapper-view block-photo face-photo-step">
|
||||
<div class="facephoto view">
|
||||
<h3 class="title"><%- gettext( "Take Your Photo" ) %></h3>
|
||||
<p><%= gettext("Course key: ") + courseKey %></p>
|
||||
<p><%= gettext("Checkpoint name: ") + checkpointName %></p>
|
||||
<div class="instruction">
|
||||
<p><%- gettext( "Use your webcam to take a photo of your face. We will match this photo with the photo on your ID." ) %></p>
|
||||
</div>
|
||||
|
||||
<div class="wrapper-task">
|
||||
<div id="webcam" class="task cam"></div>
|
||||
|
||||
<div class="wrapper-help">
|
||||
<div class="help help-task photo-tips facetips">
|
||||
<h4 class="title"><%- gettext( "Tips on taking a successful photo" ) %></h4>
|
||||
|
||||
<div class="copy">
|
||||
<ul class="list-help">
|
||||
<li class="help-item"><%- gettext( "Make sure your face is well-lit" ) %></li>
|
||||
<li class="help-item"><%- gettext( "Be sure your entire face is inside the frame" ) %></li>
|
||||
<li class="help-item">
|
||||
<%= _.sprintf( gettext( "Once in position, use the camera button %(icon)s to capture your photo" ), { icon: '<span class="example">(<i class="icon fa fa-camera" aria-hidden="true"></i>)</span>' } ) %>
|
||||
</li>
|
||||
<li class="help-item"><%- gettext( "Can we match the photo you took with the one on your ID?" ) %></li>
|
||||
<li class="help-item"><%- gettext( "Use the retake photo button if you are not pleased with your photo" ) %></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help help-faq facefaq">
|
||||
<h4 class="sr title"><%- gettext( "Frequently Asked Questions" ) %></h4>
|
||||
<div class="copy">
|
||||
<dl class="list-faq">
|
||||
<dt class="faq-question">
|
||||
<%- _.sprintf( gettext( "Why does %(platformName)s need my photo?" ), { platformName: platformName } ) %>
|
||||
</dt>
|
||||
<dd class="faq-answer"><%- gettext( "As part of the verification process, you take a photo of both your face and a government-issued photo ID. Our authorization service confirms your identity by comparing the photo you take with the photo on your ID." ) %></dd>
|
||||
<dt class="faq-question">
|
||||
<%- _.sprintf( gettext( "What does %(platformName)s do with this photo?" ), { platformName: platformName } ) %>
|
||||
</dt>
|
||||
<dd class="faq-answer"><%- _.sprintf( gettext( "We use the highest levels of security available to encrypt your photo and send it to our authorization service for review. Your photo and information are not saved or visible anywhere on %(platformName)s after the verification process is complete." ), { platformName: platformName } ) %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<button class="action action-primary" id="submit"><%= gettext("Submit") %></button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user