Separate verification deadline from upgrade deadline
* Add verification deadline model. * Populate verification deadlines from course modes table. * Update student dashboard to use verification deadlines. * Update pay-and-verify view to use verification deadlines. * Simplify Django admin for course modes and add validation. * Add verification deadline to Django admin for course modes. * Add UI for when the upgrade deadline is missed in the pay-and-verify flow.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
# -*- 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 'HistoricalVerificationDeadline'
|
||||
db.create_table('verify_student_historicalverificationdeadline', (
|
||||
('id', self.gf('django.db.models.fields.IntegerField')(db_index=True, blank=True)),
|
||||
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
|
||||
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
|
||||
('course_key', self.gf('xmodule_django.models.CourseKeyField')(max_length=255, db_index=True)),
|
||||
('deadline', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
(u'history_id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
(u'history_date', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
(u'history_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name=u'+', null=True, on_delete=models.SET_NULL, to=orm['auth.User'])),
|
||||
(u'history_type', self.gf('django.db.models.fields.CharField')(max_length=1)),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['HistoricalVerificationDeadline'])
|
||||
|
||||
# Adding model 'VerificationDeadline'
|
||||
db.create_table('verify_student_verificationdeadline', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('created', self.gf('model_utils.fields.AutoCreatedField')(default=datetime.datetime.now)),
|
||||
('modified', self.gf('model_utils.fields.AutoLastModifiedField')(default=datetime.datetime.now)),
|
||||
('course_key', self.gf('xmodule_django.models.CourseKeyField')(unique=True, max_length=255, db_index=True)),
|
||||
('deadline', self.gf('django.db.models.fields.DateTimeField')()),
|
||||
))
|
||||
db.send_create_signal('verify_student', ['VerificationDeadline'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'HistoricalVerificationDeadline'
|
||||
db.delete_table('verify_student_historicalverificationdeadline')
|
||||
|
||||
# Deleting model 'VerificationDeadline'
|
||||
db.delete_table('verify_student_verificationdeadline')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'verify_student.historicalverificationdeadline': {
|
||||
'Meta': {'ordering': "(u'-history_date', u'-history_id')", 'object_name': 'HistoricalVerificationDeadline'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
u'history_date': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
u'history_id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
u'history_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
|
||||
u'history_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'blank': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'verify_student.incoursereverificationconfiguration': {
|
||||
'Meta': {'ordering': "('-change_date',)", '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.skippedreverification': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'SkippedReverification'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'skipped_checkpoint'", 'to': "orm['verify_student.VerificationCheckpoint']"}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'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': "'9b14470c-4219-4c69-9a38-d8ff14b60b09'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
|
||||
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
|
||||
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'verify_student.verificationcheckpoint': {
|
||||
'Meta': {'unique_together': "(('course_id', 'checkpoint_location'),)", 'object_name': 'VerificationCheckpoint'},
|
||||
'checkpoint_location': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'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.verificationdeadline': {
|
||||
'Meta': {'object_name': 'VerificationDeadline'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'verify_student.verificationstatus': {
|
||||
'Meta': {'object_name': 'VerificationStatus'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkpoint_status'", '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,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from south.utils import datetime_utils as datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"""
|
||||
This migration populates the "verification deadline" model with
|
||||
the "expiration datetime" from the course modes table for verified
|
||||
courses.
|
||||
|
||||
In the past, the course modes expiration (really an upgrade deadline)
|
||||
and the verification deadline were always set to the same value.
|
||||
With this change, the verification deadline will now be tracked in a separate
|
||||
model owned by the verify_student app.
|
||||
"""
|
||||
|
||||
# Retrieve all verified course modes (whether they have expired or not)
|
||||
# Unfortunately, we don't have access to constants from the application here,
|
||||
# so we hard-code the names of the course modes that require verification.
|
||||
verified_modes = orm['course_modes.CourseMode'].objects.filter(
|
||||
mode_slug__in=["verified", "professional"],
|
||||
expiration_datetime__isnull=False,
|
||||
)
|
||||
|
||||
for mode in verified_modes:
|
||||
orm.VerificationDeadline.objects.create(
|
||||
course_key=mode.course_id,
|
||||
deadline=mode.expiration_datetime,
|
||||
)
|
||||
|
||||
def backwards(self, orm):
|
||||
"""
|
||||
Backwards migration deletes all verification deadlines.
|
||||
"""
|
||||
orm.VerificationDeadline.objects.all().delete()
|
||||
|
||||
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'})
|
||||
},
|
||||
'course_modes.coursemode': {
|
||||
'Meta': {'unique_together': "(('course_id', 'mode_slug', 'currency'),)", 'object_name': 'CourseMode'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'expiration_date': ('django.db.models.fields.DateField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'min_price': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'mode_display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'sku': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
|
||||
'suggested_prices': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'course_modes.coursemodesarchive': {
|
||||
'Meta': {'object_name': 'CourseModesArchive'},
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'currency': ('django.db.models.fields.CharField', [], {'default': "'usd'", 'max_length': '8'}),
|
||||
'expiration_date': ('django.db.models.fields.DateField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'expiration_datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'min_price': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'mode_display_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'mode_slug': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'suggested_prices': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'verify_student.historicalverificationdeadline': {
|
||||
'Meta': {'ordering': "(u'-history_date', u'-history_id')", 'object_name': 'HistoricalVerificationDeadline'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
u'history_date': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
u'history_id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
u'history_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
|
||||
u'history_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
|
||||
'id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'blank': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'verify_student.incoursereverificationconfiguration': {
|
||||
'Meta': {'ordering': "('-change_date',)", '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.skippedreverification': {
|
||||
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'SkippedReverification'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'skipped_checkpoint'", 'to': "orm['verify_student.VerificationCheckpoint']"}),
|
||||
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
|
||||
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'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': "'6644e0c2-da9b-49a4-9d0c-c19c596c911e'", 'max_length': '255', 'db_index': 'True'}),
|
||||
'reviewing_service': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'reviewing_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'photo_verifications_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}),
|
||||
'status': ('model_utils.fields.StatusField', [], {'default': "'created'", 'max_length': '100', u'no_check_for_status': 'True'}),
|
||||
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
|
||||
'submitted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
|
||||
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
|
||||
},
|
||||
'verify_student.verificationcheckpoint': {
|
||||
'Meta': {'unique_together': "(('course_id', 'checkpoint_location'),)", 'object_name': 'VerificationCheckpoint'},
|
||||
'checkpoint_location': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'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.verificationdeadline': {
|
||||
'Meta': {'object_name': 'VerificationDeadline'},
|
||||
'course_key': ('xmodule_django.models.CourseKeyField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
|
||||
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
|
||||
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'})
|
||||
},
|
||||
'verify_student.verificationstatus': {
|
||||
'Meta': {'object_name': 'VerificationStatus'},
|
||||
'checkpoint': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkpoint_status'", '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 = ['course_modes', 'verify_student']
|
||||
symmetrical = True
|
||||
@@ -24,14 +24,17 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from boto.s3.connection import S3Connection
|
||||
from boto.s3.key import Key
|
||||
from simple_history.models import HistoricalRecords
|
||||
from config_models.models import ConfigurationModel
|
||||
from course_modes.models import CourseMode
|
||||
from model_utils.models import StatusModel
|
||||
from model_utils.models import StatusModel, TimeStampedModel
|
||||
from model_utils import Choices
|
||||
from verify_student.ssencrypt import (
|
||||
random_aes_key, encrypt_and_encode,
|
||||
@@ -884,6 +887,119 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
return 'ID Verified'
|
||||
|
||||
|
||||
class VerificationDeadline(TimeStampedModel):
|
||||
"""
|
||||
Represent a verification deadline for a particular course.
|
||||
|
||||
The verification deadline is the datetime after which
|
||||
users are no longer allowed to submit photos for initial verification
|
||||
in a course.
|
||||
|
||||
Note that this is NOT the same as the "upgrade" deadline, after
|
||||
which a user is no longer allowed to upgrade to a verified enrollment.
|
||||
|
||||
If no verification deadline record exists for a course,
|
||||
then that course does not have a deadline. This means that users
|
||||
can submit photos at any time.
|
||||
"""
|
||||
|
||||
course_key = CourseKeyField(
|
||||
max_length=255,
|
||||
db_index=True,
|
||||
unique=True,
|
||||
help_text=ugettext_lazy(u"The course for which this deadline applies"),
|
||||
)
|
||||
|
||||
deadline = models.DateTimeField(
|
||||
help_text=ugettext_lazy(
|
||||
u"The datetime after which users are no longer allowed "
|
||||
u"to submit photos for verification."
|
||||
)
|
||||
)
|
||||
|
||||
# Maintain a history of changes to deadlines for auditing purposes
|
||||
history = HistoricalRecords()
|
||||
|
||||
ALL_DEADLINES_CACHE_KEY = "verify_student.all_verification_deadlines"
|
||||
|
||||
@classmethod
|
||||
def set_deadline(cls, course_key, deadline):
|
||||
"""
|
||||
Configure the verification deadline for a course.
|
||||
|
||||
If `deadline` is `None`, then the course will have no verification
|
||||
deadline. In this case, users will be able to verify for the course
|
||||
at any time.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): Identifier for the course.
|
||||
deadline (datetime or None): The verification deadline.
|
||||
|
||||
"""
|
||||
if deadline is None:
|
||||
VerificationDeadline.objects.filter(course_key=course_key).delete()
|
||||
else:
|
||||
record, created = VerificationDeadline.objects.get_or_create(
|
||||
course_key=course_key,
|
||||
defaults={"deadline": deadline}
|
||||
)
|
||||
|
||||
if not created:
|
||||
record.deadline = deadline
|
||||
record.save()
|
||||
|
||||
@classmethod
|
||||
def deadlines_for_courses(cls, course_keys):
|
||||
"""
|
||||
Retrieve verification deadlines for particular courses.
|
||||
|
||||
Arguments:
|
||||
course_keys (list): List of `CourseKey`s.
|
||||
|
||||
Returns:
|
||||
dict: Map of course keys to datetimes (verification deadlines)
|
||||
|
||||
"""
|
||||
all_deadlines = cache.get(cls.ALL_DEADLINES_CACHE_KEY)
|
||||
if all_deadlines is None:
|
||||
all_deadlines = {
|
||||
deadline.course_key: deadline.deadline
|
||||
for deadline in VerificationDeadline.objects.all()
|
||||
}
|
||||
cache.set(cls.ALL_DEADLINES_CACHE_KEY, all_deadlines)
|
||||
|
||||
return {
|
||||
course_key: all_deadlines[course_key]
|
||||
for course_key in course_keys
|
||||
if course_key in all_deadlines
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def deadline_for_course(cls, course_key):
|
||||
"""
|
||||
Retrieve the verification deadline for a particular course.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The identifier for the course.
|
||||
|
||||
Returns:
|
||||
datetime or None
|
||||
|
||||
"""
|
||||
try:
|
||||
deadline = cls.objects.get(course_key=course_key)
|
||||
return deadline.deadline
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
@receiver(models.signals.post_save, sender=VerificationDeadline)
|
||||
@receiver(models.signals.post_delete, sender=VerificationDeadline)
|
||||
def invalidate_deadline_caches(sender, **kwargs): # pylint: disable=unused-argument
|
||||
"""Invalidate the cached verification deadline information. """
|
||||
cache.delete(VerificationDeadline.ALL_DEADLINES_CACHE_KEY)
|
||||
|
||||
|
||||
class VerificationCheckpoint(models.Model):
|
||||
"""Represents a point at which a user is asked to re-verify his/her
|
||||
identity.
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from mock import patch
|
||||
from nose.tools import assert_is_none, assert_equals, assert_raises, assert_true, assert_false # pylint: disable=no-name-in-module
|
||||
|
||||
@@ -14,9 +15,13 @@ from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification, VerificationException, VerificationCheckpoint, VerificationStatus,
|
||||
SkippedReverification
|
||||
SoftwareSecurePhotoVerification,
|
||||
VerificationException, VerificationCheckpoint,
|
||||
VerificationStatus, SkippedReverification,
|
||||
VerificationDeadline
|
||||
)
|
||||
|
||||
FAKE_SETTINGS = {
|
||||
@@ -763,3 +768,45 @@ class SkippedReverificationTest(ModuleStoreTestCase):
|
||||
self.assertFalse(
|
||||
SkippedReverification.check_user_skipped_reverification_exists(course_id=self.course.id, user=user2)
|
||||
)
|
||||
|
||||
|
||||
class VerificationDeadlineTest(TestCase):
|
||||
"""
|
||||
Tests for the VerificationDeadline model.
|
||||
"""
|
||||
|
||||
def test_caching(self):
|
||||
deadlines = {
|
||||
CourseKey.from_string("edX/DemoX/Fall"): datetime.now(pytz.UTC),
|
||||
CourseKey.from_string("edX/DemoX/Spring"): datetime.now(pytz.UTC) + timedelta(days=1)
|
||||
}
|
||||
course_keys = deadlines.keys()
|
||||
|
||||
# Initially, no deadlines are set
|
||||
with self.assertNumQueries(1):
|
||||
all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
|
||||
self.assertEqual(all_deadlines, {})
|
||||
|
||||
# Create the deadlines
|
||||
for course_key, deadline in deadlines.iteritems():
|
||||
VerificationDeadline.objects.create(
|
||||
course_key=course_key,
|
||||
deadline=deadline,
|
||||
)
|
||||
|
||||
# Warm the cache
|
||||
with self.assertNumQueries(1):
|
||||
VerificationDeadline.deadlines_for_courses(course_keys)
|
||||
|
||||
# Load the deadlines from the cache
|
||||
with self.assertNumQueries(0):
|
||||
all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
|
||||
self.assertEqual(all_deadlines, deadlines)
|
||||
|
||||
# Delete the deadlines
|
||||
VerificationDeadline.objects.all().delete()
|
||||
|
||||
# Verify that the deadlines are updated correctly
|
||||
with self.assertNumQueries(1):
|
||||
all_deadlines = VerificationDeadline.deadlines_for_courses(course_keys)
|
||||
self.assertEqual(all_deadlines, {})
|
||||
|
||||
@@ -43,8 +43,9 @@ from verify_student.views import (
|
||||
_compose_message_reverification_email
|
||||
)
|
||||
from verify_student.models import (
|
||||
SoftwareSecurePhotoVerification, VerificationCheckpoint,
|
||||
InCourseReverificationConfiguration, VerificationStatus
|
||||
VerificationDeadline, SoftwareSecurePhotoVerification,
|
||||
VerificationCheckpoint, InCourseReverificationConfiguration,
|
||||
VerificationStatus
|
||||
)
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
@@ -612,15 +613,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
||||
self._assert_contribution_amount(response, "12.34")
|
||||
|
||||
def test_verification_deadline(self):
|
||||
# Set a deadline on the course mode
|
||||
deadline = datetime(2999, 1, 2, tzinfo=pytz.UTC)
|
||||
course = self._create_course("verified")
|
||||
mode = CourseMode.objects.get(
|
||||
course_id=course.id,
|
||||
mode_slug="verified"
|
||||
)
|
||||
expiration = datetime(2999, 1, 2, tzinfo=pytz.UTC)
|
||||
mode.expiration_datetime = expiration
|
||||
mode.save()
|
||||
|
||||
# Set a deadline on the course mode AND on the verification deadline model.
|
||||
# This simulates the common case in which the upgrade deadline (course mode expiration)
|
||||
# and the verification deadline are the same.
|
||||
# NOTE: we used to use the course mode expiration datetime for BOTH of these deadlines,
|
||||
# before the VerificationDeadline model was introduced.
|
||||
self._set_deadlines(course.id, upgrade_deadline=deadline, verification_deadline=deadline)
|
||||
|
||||
# Expect that the expiration date is set
|
||||
response = self._get_page("verify_student_start_flow", course.id)
|
||||
@@ -628,14 +629,13 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.assertEqual(data['verification_deadline'], "Jan 02, 2999 at 00:00 UTC")
|
||||
|
||||
def test_course_mode_expired(self):
|
||||
deadline = datetime(1999, 1, 2, tzinfo=pytz.UTC)
|
||||
course = self._create_course("verified")
|
||||
mode = CourseMode.objects.get(
|
||||
course_id=course.id,
|
||||
mode_slug="verified"
|
||||
)
|
||||
expiration = datetime(1999, 1, 2, tzinfo=pytz.UTC)
|
||||
mode.expiration_datetime = expiration
|
||||
mode.save()
|
||||
|
||||
# Set the upgrade deadline (course mode expiration) and verification deadline
|
||||
# to the same value. This used to be the default when we used the expiration datetime
|
||||
# for BOTH values.
|
||||
self._set_deadlines(course.id, upgrade_deadline=deadline, verification_deadline=deadline)
|
||||
|
||||
# Need to be enrolled
|
||||
self._enroll(course.id, "verified")
|
||||
@@ -646,6 +646,66 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.assertContains(response, "verification deadline")
|
||||
self.assertContains(response, "Jan 02, 1999 at 00:00 UTC")
|
||||
|
||||
@ddt.data(datetime(2999, 1, 2, tzinfo=pytz.UTC), None)
|
||||
def test_course_mode_expired_verification_deadline_in_future(self, verification_deadline):
|
||||
course = self._create_course("verified")
|
||||
|
||||
# Set the upgrade deadline in the past, but the verification
|
||||
# deadline in the future.
|
||||
self._set_deadlines(
|
||||
course.id,
|
||||
upgrade_deadline=datetime(1999, 1, 2, tzinfo=pytz.UTC),
|
||||
verification_deadline=verification_deadline,
|
||||
)
|
||||
|
||||
# Try to pay or upgrade.
|
||||
# We should get an error message since the deadline has passed.
|
||||
for page_name in ["verify_student_start_flow", "verify_student_upgrade_and_verify"]:
|
||||
response = self._get_page(page_name, course.id)
|
||||
self.assertContains(response, "Upgrade Deadline Has Passed")
|
||||
|
||||
# Simulate paying for the course and enrolling
|
||||
self._enroll(course.id, "verified")
|
||||
|
||||
# Enter the verification part of the flow
|
||||
# Expect that we are able to verify
|
||||
response = self._get_page("verify_student_verify_now", course.id)
|
||||
self.assertNotContains(response, "Verification is no longer available")
|
||||
|
||||
data = self._get_page_data(response)
|
||||
self.assertEqual(data['message_key'], PayAndVerifyView.VERIFY_NOW_MSG)
|
||||
|
||||
# Check that the verification deadline (rather than the upgrade deadline) is displayed
|
||||
if verification_deadline is not None:
|
||||
self.assertEqual(data["verification_deadline"], "Jan 02, 2999 at 00:00 UTC")
|
||||
else:
|
||||
self.assertEqual(data["verification_deadline"], "")
|
||||
|
||||
def test_course_mode_not_expired_verification_deadline_passed(self):
|
||||
course = self._create_course("verified")
|
||||
|
||||
# Set the upgrade deadline in the future
|
||||
# and the verification deadline in the past
|
||||
# We try not to discourage this with validation rules,
|
||||
# since it's a bad user experience
|
||||
# to purchase a verified track and then not be able to verify,
|
||||
# but if it happens we need to handle it gracefully.
|
||||
self._set_deadlines(
|
||||
course.id,
|
||||
upgrade_deadline=datetime(2999, 1, 2, tzinfo=pytz.UTC),
|
||||
verification_deadline=datetime(1999, 1, 2, tzinfo=pytz.UTC),
|
||||
)
|
||||
|
||||
# Enroll as verified (simulate purchasing the verified enrollment)
|
||||
self._enroll(course.id, "verified")
|
||||
|
||||
# Even though the upgrade deadline is in the future,
|
||||
# the verification deadline has passed, so we should see an error
|
||||
# message when we go to verify.
|
||||
response = self._get_page("verify_student_verify_now", course.id)
|
||||
self.assertContains(response, "verification deadline")
|
||||
self.assertContains(response, "Jan 02, 1999 at 00:00 UTC")
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'EMBARGO': True})
|
||||
def test_embargo_restrict(self):
|
||||
course = self._create_course("verified")
|
||||
@@ -716,6 +776,30 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
||||
attempt.created_at = datetime.now(pytz.UTC) - timedelta(days=(days_good_for + 1))
|
||||
attempt.save()
|
||||
|
||||
def _set_deadlines(self, course_key, upgrade_deadline=None, verification_deadline=None):
|
||||
"""
|
||||
Set the upgrade and verification deadlines.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): Identifier for the course.
|
||||
|
||||
Keyword Arguments:
|
||||
|
||||
upgrade_deadline (datetime): Datetime after which a user cannot
|
||||
upgrade to a verified mode.
|
||||
|
||||
verification_deadline (datetime): Datetime after which a user cannot
|
||||
submit an initial verification attempt.
|
||||
|
||||
"""
|
||||
# Set the course mode expiration (same as the "upgrade" deadline)
|
||||
mode = CourseMode.objects.get(course_id=course_key, mode_slug="verified")
|
||||
mode.expiration_datetime = upgrade_deadline
|
||||
mode.save()
|
||||
|
||||
# Set the verification deadline
|
||||
VerificationDeadline.set_deadline(course_key, verification_deadline)
|
||||
|
||||
def _set_contribution(self, amount, course_id):
|
||||
"""Set the contribution amount pre-filled in a session var. """
|
||||
session = self.client.session
|
||||
@@ -785,6 +869,15 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
|
||||
"""Retrieve the data attributes rendered on the page. """
|
||||
soup = BeautifulSoup(response.content)
|
||||
pay_and_verify_div = soup.find(id="pay-and-verify-container")
|
||||
|
||||
self.assertIsNot(
|
||||
pay_and_verify_div, None,
|
||||
msg=(
|
||||
"Could not load pay and verify flow data. "
|
||||
"Maybe this isn't the pay and verify page?"
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
'full_name': pay_and_verify_div['data-full-name'],
|
||||
'course_key': pay_and_verify_div['data-course-key'],
|
||||
|
||||
@@ -47,6 +47,7 @@ from shoppingcart.processors import (
|
||||
)
|
||||
from verify_student.ssencrypt import has_valid_signature
|
||||
from verify_student.models import (
|
||||
VerificationDeadline,
|
||||
SoftwareSecurePhotoVerification,
|
||||
VerificationCheckpoint,
|
||||
VerificationStatus,
|
||||
@@ -194,6 +195,10 @@ class PayAndVerifyView(View):
|
||||
FACE_PHOTO_STEP: [WEBCAM_REQ],
|
||||
}
|
||||
|
||||
# Deadline types
|
||||
VERIFICATION_DEADLINE = "verification"
|
||||
UPGRADE_DEADLINE = "upgrade"
|
||||
|
||||
@method_decorator(login_required)
|
||||
def get(
|
||||
self, request, course_id,
|
||||
@@ -201,7 +206,8 @@ class PayAndVerifyView(View):
|
||||
current_step=None,
|
||||
message=FIRST_TIME_VERIFY_MSG
|
||||
):
|
||||
"""Render the pay/verify requirements page.
|
||||
"""
|
||||
Render the payment and verification flow.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest): The request object.
|
||||
@@ -244,28 +250,45 @@ class PayAndVerifyView(View):
|
||||
if redirect_url:
|
||||
return redirect(redirect_url)
|
||||
|
||||
expired_verified_course_mode, unexpired_paid_course_mode = self._get_expired_verified_and_paid_mode(course_key)
|
||||
# If the verification deadline has passed
|
||||
# then show the user a message that he/she can't verify.
|
||||
#
|
||||
# We're making the assumptions (enforced in Django admin) that:
|
||||
#
|
||||
# 1) Only verified modes have verification deadlines.
|
||||
#
|
||||
# 2) If set, verification deadlines are always AFTER upgrade deadlines, because why would you
|
||||
# let someone upgrade into a verified track if they can't complete verification?
|
||||
#
|
||||
verification_deadline = VerificationDeadline.deadline_for_course(course.id)
|
||||
response = self._response_if_deadline_passed(course, self.VERIFICATION_DEADLINE, verification_deadline)
|
||||
if response is not None:
|
||||
log.info(u"Verification deadline for '%s' has passed.", course.id)
|
||||
return response
|
||||
|
||||
# Check that the course has an unexpired paid mode
|
||||
if unexpired_paid_course_mode is not None:
|
||||
if CourseMode.is_verified_mode(unexpired_paid_course_mode):
|
||||
# Retrieve the relevant course mode for the payment/verification flow.
|
||||
#
|
||||
# WARNING: this is technical debt! A much better way to do this would be to
|
||||
# separate out the payment flow and use the product SKU to figure out what
|
||||
# the user is trying to purchase.
|
||||
#
|
||||
# Nonethless, for the time being we continue to make the really ugly assumption
|
||||
# that at some point there was a paid course mode we can query for the price.
|
||||
relevant_course_mode = self._get_paid_mode(course_key)
|
||||
|
||||
# If we can find a relevant course mode, then log that we're entering the flow
|
||||
# Otherwise, this course does not support payment/verification, so respond with a 404.
|
||||
if relevant_course_mode is not None:
|
||||
if CourseMode.is_verified_mode(relevant_course_mode):
|
||||
log.info(
|
||||
u"Entering verified workflow for user '%s', course '%s', with current step '%s'.",
|
||||
u"Entering payment and verification flow for user '%s', course '%s', with current step '%s'.",
|
||||
request.user.id, course_id, current_step
|
||||
)
|
||||
elif expired_verified_course_mode is not None:
|
||||
# Check if there is an *expired* verified course mode;
|
||||
# if so, we should show a message explaining that the verification
|
||||
# deadline has passed.
|
||||
log.info(u"Verification deadline for '%s' has passed.", course_id)
|
||||
context = {
|
||||
'course': course,
|
||||
'deadline': (
|
||||
get_default_time_display(expired_verified_course_mode.expiration_datetime)
|
||||
if expired_verified_course_mode.expiration_datetime else ""
|
||||
else:
|
||||
log.info(
|
||||
u"Entering payment flow for user '%s', course '%s', with current step '%s'",
|
||||
request.user.id, course_id, current_step
|
||||
)
|
||||
}
|
||||
return render_to_response("verify_student/missed_verification_deadline.html", context)
|
||||
else:
|
||||
# Otherwise, there has never been a verified/paid mode,
|
||||
# so return a page not found response.
|
||||
@@ -275,14 +298,32 @@ class PayAndVerifyView(View):
|
||||
)
|
||||
raise Http404
|
||||
|
||||
# If the user is trying to *pay* and the upgrade deadline has passed,
|
||||
# then they shouldn't be able to enter the flow.
|
||||
#
|
||||
# NOTE: This should match the availability dates used by the E-Commerce service
|
||||
# to determine whether a user can purchase a product. The idea is that if the service
|
||||
# won't fulfill the order, we shouldn't even let the user get into the payment flow.
|
||||
#
|
||||
user_is_trying_to_pay = message in [self.FIRST_TIME_VERIFY_MSG, self.UPGRADE_MSG]
|
||||
if user_is_trying_to_pay:
|
||||
upgrade_deadline = relevant_course_mode.expiration_datetime
|
||||
response = self._response_if_deadline_passed(course, self.UPGRADE_DEADLINE, upgrade_deadline)
|
||||
if response is not None:
|
||||
log.info(u"Upgrade deadline for '%s' has passed.", course.id)
|
||||
return response
|
||||
|
||||
# Check whether the user has verified, paid, and enrolled.
|
||||
# A user is considered "paid" if he or she has an enrollment
|
||||
# with a paid course mode (such as "verified").
|
||||
# For this reason, every paid user is enrolled, but not
|
||||
# every enrolled user is paid.
|
||||
# If the course mode is not verified(i.e only paid) then already_verified is always True
|
||||
already_verified = self._check_already_verified(request.user) \
|
||||
if CourseMode.is_verified_mode(unexpired_paid_course_mode) else True
|
||||
already_verified = (
|
||||
self._check_already_verified(request.user)
|
||||
if CourseMode.is_verified_mode(relevant_course_mode)
|
||||
else True
|
||||
)
|
||||
already_paid, is_enrolled = self._check_enrollment(request.user, course_key)
|
||||
|
||||
# Redirect the user to a more appropriate page if the
|
||||
@@ -302,7 +343,7 @@ class PayAndVerifyView(View):
|
||||
always_show_payment,
|
||||
already_verified,
|
||||
already_paid,
|
||||
unexpired_paid_course_mode
|
||||
relevant_course_mode
|
||||
)
|
||||
requirements = self._requirements(display_steps, request.user.is_active)
|
||||
|
||||
@@ -346,7 +387,7 @@ class PayAndVerifyView(View):
|
||||
verification_good_until = self._verification_valid_until(request.user)
|
||||
|
||||
# get available payment processors
|
||||
if unexpired_paid_course_mode.sku:
|
||||
if relevant_course_mode.sku:
|
||||
# transaction will be conducted via ecommerce service
|
||||
processors = ecommerce_api_client(request.user).payment.processors.get()
|
||||
else:
|
||||
@@ -358,7 +399,7 @@ class PayAndVerifyView(View):
|
||||
'contribution_amount': contribution_amount,
|
||||
'course': course,
|
||||
'course_key': unicode(course_key),
|
||||
'course_mode': unexpired_paid_course_mode,
|
||||
'course_mode': relevant_course_mode,
|
||||
'courseware_url': courseware_url,
|
||||
'current_step': current_step,
|
||||
'disable_courseware_js': True,
|
||||
@@ -370,8 +411,8 @@ class PayAndVerifyView(View):
|
||||
'requirements': requirements,
|
||||
'user_full_name': full_name,
|
||||
'verification_deadline': (
|
||||
get_default_time_display(unexpired_paid_course_mode.expiration_datetime)
|
||||
if unexpired_paid_course_mode.expiration_datetime else ""
|
||||
get_default_time_display(verification_deadline)
|
||||
if verification_deadline else ""
|
||||
),
|
||||
'already_verified': already_verified,
|
||||
'verification_good_until': verification_good_until,
|
||||
@@ -449,41 +490,35 @@ class PayAndVerifyView(View):
|
||||
if url is not None:
|
||||
return redirect(url)
|
||||
|
||||
def _get_expired_verified_and_paid_mode(self, course_key): # pylint: disable=invalid-name
|
||||
"""Retrieve expired verified mode and unexpired paid mode(with min_price>0) for a course.
|
||||
def _get_paid_mode(self, course_key):
|
||||
"""
|
||||
Retrieve the paid course mode for a course.
|
||||
|
||||
The returned course mode may or may not be expired.
|
||||
Unexpired modes are preferred to expired modes.
|
||||
|
||||
Arguments:
|
||||
course_key (CourseKey): The location of the course.
|
||||
|
||||
Returns:
|
||||
Tuple of `(expired_verified_mode, unexpired_paid_mode)`. If provided,
|
||||
`expired_verified_mode` is an *expired* verified mode for the course.
|
||||
If provided, `unexpired_paid_mode` is an *unexpired* paid(with min_price>0)
|
||||
mode for the course. Either of these may be None.
|
||||
CourseMode tuple
|
||||
|
||||
"""
|
||||
# Retrieve all the modes at once to reduce the number of database queries
|
||||
all_modes, unexpired_modes = CourseMode.all_and_unexpired_modes_for_courses([course_key])
|
||||
|
||||
# Unexpired paid modes
|
||||
unexpired_paid_modes = [mode for mode in unexpired_modes[course_key] if mode.min_price]
|
||||
if len(unexpired_paid_modes) > 1:
|
||||
# There is more than one paid mode defined,
|
||||
# so choose the first one.
|
||||
log.warn(
|
||||
u"More than one paid modes are defined for course '%s' choosing the first one %s",
|
||||
course_key, unexpired_paid_modes[0]
|
||||
)
|
||||
unexpired_paid_mode = unexpired_paid_modes[0] if unexpired_paid_modes else None
|
||||
# Retrieve the first unexpired, paid mode, if there is one
|
||||
for mode in unexpired_modes[course_key]:
|
||||
if mode.min_price > 0:
|
||||
return mode
|
||||
|
||||
# Find an unexpired verified mode
|
||||
verified_mode = CourseMode.verified_mode_for_course(course_key, modes=unexpired_modes[course_key])
|
||||
expired_verified_mode = None
|
||||
# Otherwise, find the first expired mode
|
||||
for mode in all_modes[course_key]:
|
||||
if mode.min_price > 0:
|
||||
return mode
|
||||
|
||||
if verified_mode is None:
|
||||
expired_verified_mode = CourseMode.verified_mode_for_course(course_key, modes=all_modes[course_key])
|
||||
|
||||
return (expired_verified_mode, unexpired_paid_mode)
|
||||
# Otherwise, return None and so the view knows to respond with a 404.
|
||||
return None
|
||||
|
||||
def _display_steps(self, always_show_payment, already_verified, already_paid, course_mode):
|
||||
"""Determine which steps to display to the user.
|
||||
@@ -610,12 +645,43 @@ class PayAndVerifyView(View):
|
||||
has_paid = False
|
||||
|
||||
if enrollment_mode is not None and is_active:
|
||||
all_modes = CourseMode.modes_for_course_dict(course_key)
|
||||
all_modes = CourseMode.modes_for_course_dict(course_key, include_expired=True)
|
||||
course_mode = all_modes.get(enrollment_mode)
|
||||
has_paid = (course_mode and course_mode.min_price > 0)
|
||||
|
||||
return (has_paid, bool(is_active))
|
||||
|
||||
def _response_if_deadline_passed(self, course, deadline_name, deadline_datetime):
|
||||
"""
|
||||
Respond with some error messaging if the deadline has passed.
|
||||
|
||||
Arguments:
|
||||
course (Course): The course the user is trying to enroll in.
|
||||
deadline_name (str): One of the deadline constants.
|
||||
deadline_datetime (datetime): The deadline.
|
||||
|
||||
Returns: HttpResponse or None
|
||||
|
||||
"""
|
||||
if deadline_name not in [self.VERIFICATION_DEADLINE, self.UPGRADE_DEADLINE]:
|
||||
log.error("Invalid deadline name %s. Skipping check for whether the deadline passed.", deadline_name)
|
||||
return None
|
||||
|
||||
deadline_passed = (
|
||||
deadline_datetime is not None and
|
||||
deadline_datetime < datetime.datetime.now(UTC)
|
||||
)
|
||||
if deadline_passed:
|
||||
context = {
|
||||
'course': course,
|
||||
'deadline_name': deadline_name,
|
||||
'deadline': (
|
||||
get_default_time_display(deadline_datetime)
|
||||
if deadline_datetime else ""
|
||||
)
|
||||
}
|
||||
return render_to_response("verify_student/missed_deadline.html", context)
|
||||
|
||||
|
||||
def checkout_with_ecommerce_service(user, course_key, course_mode, processor): # pylint: disable=invalid-name
|
||||
""" Create a new basket and trigger immediate checkout, using the E-Commerce API. """
|
||||
|
||||
29
lms/templates/verify_student/missed_deadline.html
Normal file
29
lms/templates/verify_student/missed_deadline.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<%!
|
||||
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="pagetitle">
|
||||
% if deadline_name == PayAndVerifyView.VERIFICATION_DEADLINE:
|
||||
${_("Verification Deadline Has Passed")}
|
||||
% elif deadline_name == PayAndVerifyView.UPGRADE_DEADLINE:
|
||||
${_("Upgrade Deadline Has Passed")}
|
||||
% endif
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<section class="outside-app">
|
||||
<p>
|
||||
|
||||
% if deadline_name == PayAndVerifyView.VERIFICATION_DEADLINE:
|
||||
${_(u"The verification deadline for {course_name} was {date}. Verification is no longer available.").format(
|
||||
course_name=course.display_name, date=deadline)}
|
||||
% elif deadline_name == PayAndVerifyView.UPGRADE_DEADLINE:
|
||||
${_(u"The deadline to upgrade to a verified certificate for this course has passed. You can still earn an honor code certificate.")}
|
||||
% endif
|
||||
</p>
|
||||
</section>
|
||||
</%block>
|
||||
@@ -1,18 +0,0 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%inherit file="../main.html" />
|
||||
|
||||
<%block name="pagetitle">${_("Verification Deadline Has Passed")}</%block>
|
||||
|
||||
<%block name="content">
|
||||
<section class="outside-app">
|
||||
<p>${_(
|
||||
u"The verification deadline for {course_name} was {date}. "
|
||||
u"Verification is no longer available."
|
||||
).format(
|
||||
course_name=course.display_name,
|
||||
date=deadline
|
||||
)}</p>
|
||||
</section>
|
||||
</%block>
|
||||
Reference in New Issue
Block a user