From d0585be1449935124573798e6724869b260f55ec Mon Sep 17 00:00:00 2001 From: Adam Stankiewicz <agstanki@gmail.com> Date: Thu, 5 Apr 2018 16:19:31 -0400 Subject: [PATCH] ENT-943 refactor PhotoVerification model --- .../migrations/0006_ssoverification.py | 34 +++++++ lms/djangoapps/verify_student/models.py | 95 +++++++++++++++---- 2 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 lms/djangoapps/verify_student/migrations/0006_ssoverification.py diff --git a/lms/djangoapps/verify_student/migrations/0006_ssoverification.py b/lms/djangoapps/verify_student/migrations/0006_ssoverification.py new file mode 100644 index 00000000000..40e19488fa4 --- /dev/null +++ b/lms/djangoapps/verify_student/migrations/0006_ssoverification.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.12 on 2018-04-11 15:20 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('verify_student', '0005_remove_deprecated_models'), + ] + + operations = [ + migrations.CreateModel( + name='SSOVerification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', model_utils.fields.StatusField(choices=[(b'created', b'created'), (b'ready', b'ready'), (b'submitted', b'submitted'), (b'must_retry', b'must_retry'), (b'approved', b'approved'), (b'denied', b'denied')], default=b'created', max_length=100, no_check_for_status=True, verbose_name='status')), + ('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')), + ('name', models.CharField(blank=True, max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('identity_provider_type', models.CharField(choices=[(b'third_party_auth.models.OAuth2ProviderConfig', b'OAuth2 Provider'), (b'third_party_auth.models.SAMLProviderConfig', b'SAML Provider'), (b'third_party_auth.models.LTIProviderConfig', b'LTI Provider')], default=b'third_party_auth.models.SAMLProviderConfig', help_text=b'Specifies which type of Identity Provider this verification originated from.', max_length=100)), + ('identity_provider_slug', models.SlugField(default=b'default', help_text=b'The slug uniquely identifying the Identity Provider this verification originated from.', max_length=30)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 11f66dfe45d..77a7c880f80 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -88,7 +88,79 @@ def status_before_must_be(*valid_start_statuses): return decorator_func -class PhotoVerification(StatusModel): +class IDVerificationAttempt(StatusModel): + """ + Each IDVerificationAttempt represents a Student's attempt to establish + their identity through one of several methods that inherit from this Model, + including PhotoVerification and SSOVerification. + """ + STATUS = Choices('created', 'ready', 'submitted', 'must_retry', 'approved', 'denied') + user = models.ForeignKey(User, db_index=True) + + # They can change their name later on, so we want to copy the value here so + # we always preserve what it was at the time they requested. We only copy + # this value during the mark_ready() step. Prior to that, you should be + # displaying the user's name from their user.profile.name. + name = models.CharField(blank=True, max_length=255) + + created_at = models.DateTimeField(auto_now_add=True, db_index=True) + updated_at = models.DateTimeField(auto_now=True, db_index=True) + + class Meta(object): + app_label = "verify_student" + abstract = True + ordering = ['-created_at'] + + @property + def expiration_datetime(self): + """Datetime that the verification will expire. """ + days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] + return self.created_at + timedelta(days=days_good_for) + + +class SSOVerification(IDVerificationAttempt): + """ + Each SSOVerification represents a Student's attempt to establish their identity + by signing in with SSO. ID verification through SSO bypasses the need for + photo verification. + """ + + OAUTH2 = 'third_party_auth.models.OAuth2ProviderConfig' + SAML = 'third_party_auth.models.SAMLProviderConfig' + LTI = 'third_party_auth.models.LTIProviderConfig' + IDENTITY_PROVIDER_TYPE_CHOICES = ( + (OAUTH2, 'OAuth2 Provider'), + (SAML, 'SAML Provider'), + (LTI, 'LTI Provider'), + ) + + identity_provider_type = models.CharField( + max_length=100, + blank=False, + choices=IDENTITY_PROVIDER_TYPE_CHOICES, + default=SAML, + help_text=( + 'Specifies which type of Identity Provider this verification originated from.' + ) + ) + + identity_provider_slug = models.SlugField( + max_length=30, db_index=True, default='default', + help_text=( + 'The slug uniquely identifying the Identity Provider this verification originated from.' + )) + + class Meta(object): + app_label = "verify_student" + + def __unicode__(self): + return 'SSOIDVerification for {name}, status: {status}'.format( + name=self.name, + status=self.status, + ) + + +class PhotoVerification(IDVerificationAttempt): """ Each PhotoVerification represents a Student's attempt to establish their identity by uploading a photo of themselves and a picture ID. An @@ -122,7 +194,8 @@ class PhotoVerification(StatusModel): student cannot re-open this attempt -- they have to create another attempt and submit it instead. - Because this Model inherits from StatusModel, we can also do things like:: + Because this Model inherits from IDVerificationAttempt, which inherits + from StatusModel, we can also do things like: attempt.status == PhotoVerification.STATUS.created attempt.status == "created" @@ -130,15 +203,6 @@ class PhotoVerification(StatusModel): """ ######################## Fields Set During Creation ######################## # See class docstring for description of status states - STATUS = Choices('created', 'ready', 'submitted', 'must_retry', 'approved', 'denied') - user = models.ForeignKey(User, db_index=True) - - # They can change their name later on, so we want to copy the value here so - # we always preserve what it was at the time they requested. We only copy - # this value during the mark_ready() step. Prior to that, you should be - # displaying the user's name from their user.profile.name. - name = models.CharField(blank=True, max_length=255) - # Where we place the uploaded image files (e.g. S3 URLs) face_image_url = models.URLField(blank=True, max_length=255) photo_id_image_url = models.URLField(blank=True, max_length=255) @@ -152,9 +216,6 @@ class PhotoVerification(StatusModel): max_length=255, ) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - updated_at = models.DateTimeField(auto_now=True, db_index=True) - # Indicates whether or not a user wants to see the verification status # displayed on their dash. Right now, only relevant for allowing students # to "dismiss" a failed midcourse reverification message @@ -191,12 +252,6 @@ class PhotoVerification(StatusModel): abstract = True ordering = ['-created_at'] - @property - def expiration_datetime(self): - """Datetime that the verification will expire. """ - days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"] - return self.created_at + timedelta(days=days_good_for) - def active_at_datetime(self, deadline): """Check whether the verification was active at a particular datetime. -- GitLab