From 91e63ed8e0b34998dd801ea81c04af770eeb0252 Mon Sep 17 00:00:00 2001
From: Brittney Exline <bexline@edx.org>
Date: Thu, 26 Apr 2018 14:44:29 -0400
Subject: [PATCH] ENT-945 Using SSOVerifications in ID Verification flow

This reworks what was done #17930, since it had to be reverted from the IDVerificationAggregate migration.
We decided to abandon that model and directly read from both id verification models.
---
 common/djangoapps/student/helpers.py          |  14 +-
 common/djangoapps/student/views/dashboard.py  |  11 +-
 common/lib/xmodule/xmodule/seq_module.py      |   4 +-
 lms/djangoapps/certificates/signals.py        |   4 +-
 lms/djangoapps/certificates/tasks.py          |   2 +-
 .../certificates/tests/test_signals.py        |  14 +-
 .../certificates/tests/test_tasks.py          |  13 +-
 lms/djangoapps/commerce/views.py              |   2 +-
 lms/djangoapps/courseware/date_summary.py     |   4 +-
 .../tests/test_tasks_helper.py                |   4 +-
 .../0009_remove_id_verification_aggregate.py  |  26 +++
 lms/djangoapps/verify_student/models.py       |  58 ++---
 lms/djangoapps/verify_student/services.py     | 220 ++++++++----------
 .../verify_student/tests/test_models.py       |  60 +++--
 .../verify_student/tests/test_services.py     |  75 ++----
 .../verify_student/tests/test_utils.py        |  49 +++-
 lms/djangoapps/verify_student/utils.py        |  27 +++
 lms/djangoapps/verify_student/views.py        |  16 +-
 .../dashboard/_dashboard_course_listing.html  |   2 +-
 .../_dashboard_status_verification.html       |  70 +++---
 .../core/djangoapps/user_api/serializers.py   |  16 +-
 openedx/core/djangoapps/user_api/urls.py      |   4 +-
 .../user_api/verification_api/views.py        |  31 ++-
 23 files changed, 408 insertions(+), 318 deletions(-)
 create mode 100644 lms/djangoapps/verify_student/migrations/0009_remove_id_verification_aggregate.py

diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py
index 9fba0e8010c..e299a5bfb5e 100644
--- a/common/djangoapps/student/helpers.py
+++ b/common/djangoapps/student/helpers.py
@@ -116,14 +116,10 @@ def check_verify_status_by_course(user, course_enrollments):
     verifications = IDVerificationService.verifications_for_user(user)
 
     # Check whether the user has an active or pending verification attempt
-    # To avoid another database hit, we re-use the queryset we have already retrieved.
-    has_active_or_pending = IDVerificationService.user_has_valid_or_pending(
-        user, queryset=verifications
-    )
+    has_active_or_pending = IDVerificationService.user_has_valid_or_pending(user)
 
     # Retrieve expiration_datetime of most recent approved verification
-    # To avoid another database hit, we re-use the queryset we have already retrieved.
-    expiration_datetime = IDVerificationService.get_expiration_datetime(user, verifications)
+    expiration_datetime = IDVerificationService.get_expiration_datetime(user, ['approved'])
     verification_expiring_soon = is_verification_expiring_soon(expiration_datetime)
 
     # Retrieve verification deadlines for the enrolled courses
@@ -154,9 +150,12 @@ def check_verify_status_by_course(user, course_enrollments):
 
             # By default, don't show any status related to verification
             status = None
+            should_display = True
 
             # Check whether the user was approved or is awaiting approval
             if relevant_verification is not None:
+                should_display = relevant_verification.should_display_status_to_user()
+
                 if relevant_verification.status == "approved":
                     if verification_expiring_soon:
                         status = VERIFY_STATUS_NEED_TO_REVERIFY
@@ -214,7 +213,8 @@ def check_verify_status_by_course(user, course_enrollments):
 
                 status_by_course[enrollment.course_id] = {
                     'status': status,
-                    'days_until_deadline': days_until_deadline
+                    'days_until_deadline': days_until_deadline,
+                    'should_display': should_display,
                 }
 
     if recent_verification_datetime:
diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py
index 805ba14b741..78e2b4bd2c6 100644
--- a/common/djangoapps/student/views/dashboard.py
+++ b/common/djangoapps/student/views/dashboard.py
@@ -716,8 +716,8 @@ def student_dashboard(request):
 
     # Verification Attempts
     # Used to generate the "you must reverify for course x" banner
-    verification_status, verification_error_codes = IDVerificationService.user_status(user)
-    verification_errors = get_verification_error_reasons_for_display(verification_error_codes)
+    verification_status = IDVerificationService.user_status(user)
+    verification_errors = get_verification_error_reasons_for_display(verification_status['error'])
 
     # Gets data for midcourse reverifications, if any are necessary or have failed
     statuses = ["approved", "denied", "pending", "must_reverify"]
@@ -770,7 +770,9 @@ def student_dashboard(request):
         redirect_message = ''
 
     valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired']
-    display_sidebar_on_dashboard = len(order_history_list) or verification_status in valid_verification_statuses
+    display_sidebar_on_dashboard = (len(order_history_list) or
+                                    (verification_status['status'] in valid_verification_statuses and
+                                    verification_status['should_display']))
 
     # Filter out any course enrollment course cards that are associated with fulfilled entitlements
     for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]:
@@ -802,7 +804,8 @@ def student_dashboard(request):
         'credit_statuses': _credit_statuses(user, course_enrollments),
         'show_email_settings_for': show_email_settings_for,
         'reverifications': reverifications,
-        'verification_status': verification_status,
+        'verification_display': verification_status['should_display'],
+        'verification_status': verification_status['status'],
         'verification_status_by_course': verify_status_by_course,
         'verification_errors': verification_errors,
         'block_courses': block_courses,
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index b657f481600..5203d749278 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -598,9 +598,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
 
             # inject verification status
             if verification_service:
-                verification_status, __ = verification_service.get_status(user_id)
+                verification_status = verification_service.get_status(user_id)
                 context.update({
-                    'verification_status': verification_status,
+                    'verification_status': verification_status['status'],
                     'reverify_url': verification_service.reverify_url(),
                 })
 
diff --git a/lms/djangoapps/certificates/signals.py b/lms/djangoapps/certificates/signals.py
index 589e0894ccc..dc68c1ee219 100644
--- a/lms/djangoapps/certificates/signals.py
+++ b/lms/djangoapps/certificates/signals.py
@@ -82,7 +82,7 @@ def _listen_for_id_verification_status_changed(sender, user, **kwargs):  # pylin
 
     user_enrollments = CourseEnrollment.enrollments_for_user(user=user)
     grade_factory = CourseGradeFactory()
-    expected_verification_status, _ = IDVerificationService.user_status(user)
+    expected_verification_status = IDVerificationService.user_status(user)
     for enrollment in user_enrollments:
         if grade_factory.read(user=user, course=enrollment.course_overview).passed:
             if fire_ungenerated_certificate_task(user, enrollment.course_id, expected_verification_status):
@@ -93,7 +93,7 @@ def _listen_for_id_verification_status_changed(sender, user, **kwargs):  # pylin
                 log.info(message.format(
                     user=user.id,
                     course=enrollment.course_id,
-                    status=expected_verification_status
+                    status=expected_verification_status['status']
                 ))
 
 
diff --git a/lms/djangoapps/certificates/tasks.py b/lms/djangoapps/certificates/tasks.py
index 69fa1083c4c..537108cc880 100644
--- a/lms/djangoapps/certificates/tasks.py
+++ b/lms/djangoapps/certificates/tasks.py
@@ -31,7 +31,7 @@ def generate_certificate(self, **kwargs):
     course_key = CourseKey.from_string(kwargs.pop('course_key'))
     expected_verification_status = kwargs.pop('expected_verification_status', None)
     if expected_verification_status:
-        actual_verification_status, _ = IDVerificationService.user_status(student)
+        actual_verification_status = IDVerificationService.user_status(student)
         if expected_verification_status != actual_verification_status:
             raise self.retry(kwargs=original_kwargs)
     generate_user_certificates(student=student, course_key=course_key, **kwargs)
diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py
index 8212b04fbf4..5c2deb051ac 100644
--- a/lms/djangoapps/certificates/tests/test_signals.py
+++ b/lms/djangoapps/certificates/tests/test_signals.py
@@ -256,12 +256,17 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase):
                     status='submitted'
                 )
                 attempt.approve()
+                expected_verification_status = {
+                    'status': 'approved',
+                    'error': '',
+                    'should_display': True,
+                }
                 mock_generate_certificate_apply_async.assert_called_with(
                     countdown=CERTIFICATE_DELAY_SECONDS,
                     kwargs={
                         'student': unicode(self.user_one.id),
                         'course_key': unicode(self.course_one.id),
-                        'expected_verification_status': SoftwareSecurePhotoVerification.STATUS.approved
+                        'expected_verification_status': unicode(expected_verification_status),
                     }
                 )
 
@@ -277,12 +282,17 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase):
                     status='submitted'
                 )
                 attempt.approve()
+                expected_verification_status = {
+                    'status': 'approved',
+                    'error': '',
+                    'should_display': True,
+                }
                 mock_generate_certificate_apply_async.assert_called_with(
                     countdown=CERTIFICATE_DELAY_SECONDS,
                     kwargs={
                         'student': unicode(self.user_two.id),
                         'course_key': unicode(self.course_two.id),
-                        'expected_verification_status': SoftwareSecurePhotoVerification.STATUS.approved
+                        'expected_verification_status': unicode(expected_verification_status),
                     }
                 )
 
diff --git a/lms/djangoapps/certificates/tests/test_tasks.py b/lms/djangoapps/certificates/tests/test_tasks.py
index 61d1db1712b..75658079534 100644
--- a/lms/djangoapps/certificates/tests/test_tasks.py
+++ b/lms/djangoapps/certificates/tests/test_tasks.py
@@ -45,13 +45,22 @@ class GenerateUserCertificateTest(TestCase):
         course_key = 'course-v1:edX+CS101+2017_T2'
         student = UserFactory()
 
+        expected_verification_status = {
+            'status': 'approved',
+            'error': '',
+            'should_display': True,
+        }
+
         kwargs = {
             'student': student.id,
             'course_key': course_key,
-            'expected_verification_status': 'approved'
+            'expected_verification_status': expected_verification_status,
         }
 
-        user_status_mock.side_effect = [('pending', ''), ('approved', '')]
+        user_status_mock.side_effect = [
+            {'status': 'pending', 'error': '', 'should_display': True},
+            {'status': 'approved', 'error': '', 'should_display': True}
+        ]
 
         generate_certificate.apply_async(kwargs=kwargs).get()
 
diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py
index c5dea7da032..0d059cf5dcc 100644
--- a/lms/djangoapps/commerce/views.py
+++ b/lms/djangoapps/commerce/views.py
@@ -92,7 +92,7 @@ def checkout_receipt(request):
         'page_title': page_title,
         'is_payment_complete': is_payment_complete,
         'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
-        'verified': IDVerificationService.verification_valid_or_pending(request.user).exists(),
+        'verified': IDVerificationService.user_has_valid_or_pending(request.user),
         'error_summary': error_summary,
         'error_text': error_text,
         'for_help_text': for_help_text,
diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py
index 5fb7b51d1cb..85dda380742 100644
--- a/lms/djangoapps/courseware/date_summary.py
+++ b/lms/djangoapps/courseware/date_summary.py
@@ -627,8 +627,8 @@ class VerificationDeadlineDate(DateSummary):
     @lazy
     def verification_status(self):
         """Return the verification status for this user."""
-        status, _ = IDVerificationService.user_status(self.user)
-        return status
+        verification_status = IDVerificationService.user_status(self.user)
+        return verification_status['status']
 
     def must_retry(self):
         """Return True if the user must re-submit verification, False otherwise."""
diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
index 4da83349c7e..992a115fab7 100644
--- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
+++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py
@@ -396,7 +396,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
 
         RequestCache.clear_request_cache()
 
-        expected_query_count = 41
+        expected_query_count = 42
         with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
             with check_mongo_calls(mongo_count):
                 with self.assertNumQueries(expected_query_count):
@@ -1999,7 +1999,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
             'failed': 3,
             'skipped': 2
         }
-        with self.assertNumQueries(106):
+        with self.assertNumQueries(114):
             self.assertCertificatesGenerated(task_input, expected_results)
 
         expected_results = {
diff --git a/lms/djangoapps/verify_student/migrations/0009_remove_id_verification_aggregate.py b/lms/djangoapps/verify_student/migrations/0009_remove_id_verification_aggregate.py
new file mode 100644
index 00000000000..613c8033a03
--- /dev/null
+++ b/lms/djangoapps/verify_student/migrations/0009_remove_id_verification_aggregate.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-04-27 16:27
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('verify_student', '0008_populate_idverificationaggregate'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='idverificationaggregate',
+            name='content_type',
+        ),
+        migrations.RemoveField(
+            model_name='idverificationaggregate',
+            name='user',
+        ),
+        migrations.DeleteModel(
+            name='IDVerificationAggregate',
+        ),
+    ]
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index b2d51606b01..abf2859bdce 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -120,30 +120,24 @@ class IDVerificationAttempt(StatusModel):
         days_good_for = settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
         return self.created_at + timedelta(days=days_good_for)
 
+    def should_display_status_to_user(self):
+        """Whether or not the status from this attempt should be displayed to the user."""
+        raise NotImplementedError
 
-class IDVerificationAggregate(IDVerificationAttempt):
-    """
-    IDVerificationAggregate is the source of truth for all instances of IDVerificationAttempt. This
-    includes all types of verification, including PhotoVerification and SSOVerification. A generic
-    relation is used to refer to the appropriate Model object.
-    """
-    content_type = models.ForeignKey(ContentType)
-    object_id = models.PositiveIntegerField()
-    content_object = GenericForeignKey('content_type', 'object_id')
+    def active_at_datetime(self, deadline):
+        """Check whether the verification was active at a particular datetime.
 
-    # override these fields so we can set the value
-    created_at = models.DateTimeField(db_index=True)
-    updated_at = models.DateTimeField(db_index=True)
+        Arguments:
+            deadline (datetime): The date at which the verification was active
+                (created before and expiration datetime is after today).
 
-    class Meta(object):
-        app_label = "verify_student"
-        ordering = ['-created_at']
+        Returns:
+            bool
 
-    def __unicode__(self):
-        return 'IDVerificationAggregate for {name} - type: {type}, status: {status}'.format(
-            name=self.name,
-            type=self.content_type,
-            status=self.status,
+        """
+        return (
+            self.created_at < deadline and
+            self.expiration_datetime > datetime.now(pytz.UTC)
         )
 
 
@@ -188,6 +182,10 @@ class SSOVerification(IDVerificationAttempt):
             status=self.status,
         )
 
+    def should_display_status_to_user(self):
+        """Whether or not the status from this attempt should be displayed to the user."""
+        return False
+
 
 class PhotoVerification(IDVerificationAttempt, DeletableByUserValue):
     """
@@ -281,22 +279,6 @@ class PhotoVerification(IDVerificationAttempt, DeletableByUserValue):
         abstract = True
         ordering = ['-created_at']
 
-    def active_at_datetime(self, deadline):
-        """Check whether the verification was active at a particular datetime.
-
-        Arguments:
-            deadline (datetime): The date at which the verification was active
-                (created before and expiration datetime is after today).
-
-        Returns:
-            bool
-
-        """
-        return (
-            self.created_at < deadline and
-            self.expiration_datetime > datetime.now(pytz.UTC)
-        )
-
     def parsed_error_msg(self):
         """
         Sometimes, the error message we've received needs to be parsed into
@@ -849,6 +831,10 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
 
         return response
 
+    def should_display_status_to_user(self):
+        """Whether or not the status from this attempt should be displayed to the user."""
+        return True
+
 
 class VerificationDeadline(TimeStampedModel):
     """
diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py
index 0cfc20fa0a0..3105db64dbb 100644
--- a/lms/djangoapps/verify_student/services.py
+++ b/lms/djangoapps/verify_student/services.py
@@ -4,6 +4,7 @@ Implementation of abstraction layer for other parts of the system to make querie
 
 import logging
 
+from itertools import chain
 from django.conf import settings
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
@@ -12,8 +13,8 @@ from course_modes.models import CourseMode
 from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
 from student.models import User
 
-from .models import SoftwareSecurePhotoVerification
-from .utils import earliest_allowed_verification_date
+from .models import SoftwareSecurePhotoVerification, SSOVerification
+from .utils import earliest_allowed_verification_date, most_recent_verification
 
 log = logging.getLogger(__name__)
 
@@ -61,121 +62,84 @@ class IDVerificationService(object):
 
         This will check for the user's *initial* verification.
         """
-        return cls.verified_query(earliest_allowed_date).filter(user=user).exists()
+        filter_kwargs = {
+            'user': user,
+            'status': 'approved',
+            'created_at__gte': (earliest_allowed_date or earliest_allowed_verification_date())
+        }
 
-    @classmethod
-    def verified_query(cls, earliest_allowed_date=None):
-        """
-        Return a query set for all records with 'approved' state
-        that are still valid according to the earliest_allowed_date
-        value or policy settings.
-        """
-        return SoftwareSecurePhotoVerification.objects.filter(
-            status="approved",
-            created_at__gte=(earliest_allowed_date or earliest_allowed_verification_date()),
-        )
+        return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
+                SSOVerification.objects.filter(**filter_kwargs).exists())
 
     @classmethod
     def verifications_for_user(cls, user):
         """
-        Return a query set for all records associated with the given user.
+        Return a list of all verifications associated with the given user.
         """
-        return SoftwareSecurePhotoVerification.objects.filter(user=user)
+        verifications = []
+        for verification in chain(SoftwareSecurePhotoVerification.objects.filter(user=user),
+                                  SSOVerification.objects.filter(user=user)):
+            verifications.append(verification)
+        return verifications
 
     @classmethod
     def get_verified_users(cls, users):
         """
-        Return the list of user ids that have non expired verifications from the given list of users.
-        """
-        return cls.verified_query().filter(user__in=users).select_related('user')
-
-    @classmethod
-    def verification_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
-        """
-        Check whether the user has a complete verification attempt that is
-        or *might* be good. This means that it's approved, been submitted,
-        or would have been submitted but had an non-user error when it was
-        being submitted.
-        It's basically any situation in which the user has signed off on
-        the contents of the attempt, and we have not yet received a denial.
-        This will check for the user's *initial* verification.
-
-        Arguments:
-            user:
-            earliest_allowed_date: earliest allowed date given in the
-                settings
-            queryset: If a queryset is provided, that will be used instead
-                of hitting the database.
-
-        Returns:
-            queryset: queryset of 'PhotoVerification' sorted by 'created_at' in
-            descending order.
-        """
-
-        valid_statuses = ['submitted', 'approved', 'must_retry']
-
-        if queryset is None:
-            queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
-
-        return queryset.filter(
-            status__in=valid_statuses,
-            created_at__gte=(
-                earliest_allowed_date
-                or earliest_allowed_verification_date()
-            )
-        ).order_by('-created_at')
+        Return the list of users that have non-expired verifications of either type from
+        the given list of users.
+        """
+        filter_kwargs = {
+            'user__in': users,
+            'status': 'approved',
+            'created_at__gte': (earliest_allowed_verification_date())
+        }
+        return chain(
+            SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).select_related('user'),
+            SSOVerification.objects.filter(**filter_kwargs).select_related('user')
+        )
 
     @classmethod
-    def get_expiration_datetime(cls, user, queryset=None):
+    def get_expiration_datetime(cls, user, statuses):
         """
-        Check whether the user has an approved verification and return the
-        "expiration_datetime" of most recent "approved" verification.
+        Check whether the user has a verification with one of the given
+        statuses and return the "expiration_datetime" of most recent verification that
+        matches one of the given statuses.
 
         Arguments:
             user (Object): User
-            queryset: If a queryset is provided, that will be used instead
-                of hitting the database.
+            statuses: List of verification statuses (e.g., ['approved'])
 
         Returns:
-            expiration_datetime: expiration_datetime of most recent "approved"
-            verification.
+            expiration_datetime: expiration_datetime of most recent verification that
+            matches one of the given statuses.
         """
-        if queryset is None:
-            queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
+        filter_kwargs = {
+            'user': user,
+            'status__in': statuses,
+        }
+
+        photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
+        sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
 
-        photo_verification = queryset.filter(status='approved').first()
-        if photo_verification:
-            return photo_verification.expiration_datetime
+        attempt = most_recent_verification(photo_id_verifications, sso_id_verifications, 'updated_at')
+        return attempt and attempt.expiration_datetime
 
     @classmethod
-    def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
+    def user_has_valid_or_pending(cls, user):
         """
         Check whether the user has an active or pending verification attempt
 
         Returns:
             bool: True or False according to existence of valid verifications
         """
-        return cls.verification_valid_or_pending(user, earliest_allowed_date, queryset).exists()
-
-    @classmethod
-    def active_for_user(cls, user):
-        """
-        Return the most recent PhotoVerification that is marked ready (i.e. the
-        user has said they're set, but we haven't submitted anything yet).
-
-        This checks for the original verification.
-        """
-        # This should only be one at the most, but just in case we create more
-        # by mistake, we'll grab the most recently created one.
-        active_attempts = SoftwareSecurePhotoVerification.objects.filter(
-            user=user,
-            status='ready'
-        ).order_by('-created_at')
+        filter_kwargs = {
+            'user': user,
+            'status__in': ['submitted', 'approved', 'must_retry'],
+            'created_at__gte': earliest_allowed_verification_date()
+        }
 
-        if active_attempts:
-            return active_attempts[0]
-        else:
-            return None
+        return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
+                SSOVerification.objects.filter(**filter_kwargs).exists())
 
     @classmethod
     def user_status(cls, user):
@@ -188,46 +152,56 @@ class IDVerificationService(object):
         If the verification process is still ongoing, returns 'pending'
         If the verification has been denied and the user must resubmit photos, returns 'must_reverify'
 
-        This checks initial verifications
-        """
-        status = 'none'
-        error_msg = ''
+        This checks most recent verification
+        """
+        # should_display only refers to displaying the verification attempt status to a user
+        # once a verification attempt has been made, otherwise we will display a prompt to complete ID verification.
+        user_status = {
+            'status': 'none',
+            'error': '',
+            'should_display': True,
+        }
+
+        # We need to check the user's most recent attempt.
+        try:
+            photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-updated_at')
+            sso_id_verifications = SSOVerification.objects.filter(user=user).order_by('-updated_at')
+
+            attempt = most_recent_verification(photo_id_verifications, sso_id_verifications, 'updated_at')
+        except IndexError:
+            # The user has no verification attempts, return the default set of data.
+            return user_status
+
+        if not attempt:
+            return user_status
+
+        user_status['should_display'] = attempt.should_display_status_to_user()
+        if attempt.created_at < earliest_allowed_verification_date():
+            if user_status['should_display']:
+                user_status['status'] = 'expired'
+                user_status['error'] = _("Your {platform_name} verification has expired.").format(
+                    platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
+                )
+            else:
+                # If we have a verification attempt that never would have displayed to the user,
+                # and that attempt is expired, then we should treat it as if the user had never verified.
+                return user_status
 
-        if cls.user_is_verified(user):
-            status = 'approved'
+        # If someone is denied their original verification attempt, they can try to reverify.
+        elif attempt.status == 'denied':
+            user_status['status'] = 'must_reverify'
+            if hasattr(attempt, 'error_msg') and attempt.error_msg:
+                user_status['error'] = attempt.parsed_error_msg()
 
-        elif cls.user_has_valid_or_pending(user):
+        elif attempt.status == 'approved':
+            user_status['status'] = 'approved'
+
+        elif attempt.status in ['submitted', 'approved', 'must_retry']:
             # user_has_valid_or_pending does include 'approved', but if we are
             # here, we know that the attempt is still pending
-            status = 'pending'
-
-        else:
-            # we need to check the most recent attempt to see if we need to ask them to do
-            # a retry
-            try:
-                attempts = SoftwareSecurePhotoVerification.objects.filter(user=user).order_by('-updated_at')
-                attempt = attempts[0]
-            except IndexError:
-                # we return 'none'
-
-                return ('none', error_msg)
-
-            if attempt.created_at < earliest_allowed_verification_date():
-                return (
-                    'expired',
-                    _("Your {platform_name} verification has expired.").format(
-                        platform_name=configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
-                    )
-                )
-
-            # If someone is denied their original verification attempt, they can try to reverify.
-            if attempt.status == 'denied':
-                status = 'must_reverify'
-
-            if attempt.error_msg:
-                error_msg = attempt.parsed_error_msg()
+            user_status['status'] = 'pending'
 
-        return (status, error_msg)
+        return user_status
 
     @classmethod
     def verification_status_for_user(cls, user, user_enrollment_mode, user_is_verified=None):
diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py
index dde18aa15c9..c7a52dfca7a 100644
--- a/lms/djangoapps/verify_student/tests/test_models.py
+++ b/lms/djangoapps/verify_student/tests/test_models.py
@@ -8,6 +8,7 @@ import mock
 import pytz
 import requests.exceptions
 from django.conf import settings
+from django.test import TestCase
 from freezegun import freeze_time
 from mock import patch
 from nose.tools import (  # pylint: disable=no-name-in-module
@@ -21,6 +22,7 @@ from testfixtures import LogCapture
 from common.test.utils import MockS3Mixin
 from lms.djangoapps.verify_student.models import (
     SoftwareSecurePhotoVerification,
+    SSOVerification,
     VerificationDeadline,
     VerificationException
 )
@@ -95,11 +97,38 @@ def mock_software_secure_post_unavailable(url, headers=None, data=None, **kwargs
     raise requests.exceptions.ConnectionError
 
 
+class TestVerification(TestCase):
+    """
+    Common tests across all types of Verications (e.g., SoftwareSecurePhotoVerication, SSOVerification)
+    """
+    def verification_active_at_datetime(self, attempt):
+        """
+        Tests to ensure the Verification is active or inactive at the appropriate datetimes.
+        """
+        # Not active before the created date
+        before = attempt.created_at - timedelta(seconds=1)
+        self.assertFalse(attempt.active_at_datetime(before))
+
+        # Active immediately after created date
+        after_created = attempt.created_at + timedelta(seconds=1)
+        self.assertTrue(attempt.active_at_datetime(after_created))
+
+        # Active immediately before expiration date
+        expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
+        before_expiration = expiration - timedelta(seconds=1)
+        self.assertTrue(attempt.active_at_datetime(before_expiration))
+
+        # Not active after the expiration date
+        attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
+        attempt.save()
+        self.assertFalse(attempt.active_at_datetime(datetime.now(pytz.UTC) + timedelta(days=1)))
+
+
 # Lots of patching to stub in our own settings, and HTTP posting
 @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
 @patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post)
 @ddt.ddt
-class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
+class TestPhotoVerification(TestVerification, MockS3Mixin, ModuleStoreTestCase):
 
     def setUp(self):
         super(TestPhotoVerification, self).setUp()
@@ -252,24 +281,7 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
     def test_active_at_datetime(self):
         user = UserFactory.create()
         attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
-
-        # Not active before the created date
-        before = attempt.created_at - timedelta(seconds=1)
-        self.assertFalse(attempt.active_at_datetime(before))
-
-        # Active immediately after created date
-        after_created = attempt.created_at + timedelta(seconds=1)
-        self.assertTrue(attempt.active_at_datetime(after_created))
-
-        # Active immediately before expiration date
-        expiration = attempt.created_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
-        before_expiration = expiration - timedelta(seconds=1)
-        self.assertTrue(attempt.active_at_datetime(before_expiration))
-
-        # Not active after the expiration date
-        attempt.created_at = attempt.created_at - timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
-        attempt.save()
-        self.assertFalse(attempt.active_at_datetime(datetime.now(pytz.UTC) + timedelta(days=1)))
+        self.verification_active_at_datetime(attempt)
 
     def test_initial_verification_for_user(self):
         """Test that method 'get_initial_verification' of model
@@ -340,6 +352,16 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
         self.assertFalse(SoftwareSecurePhotoVerification.delete_by_user_value(user, "user"))
 
 
+class SSOVerificationTest(TestVerification):
+    """
+    Tests for the SSOVerification model
+    """
+    def test_active_at_datetime(self):
+        user = UserFactory.create()
+        attempt = SSOVerification.objects.create(user=user)
+        self.verification_active_at_datetime(attempt)
+
+
 class VerificationDeadlineTest(CacheIsolationTestCase):
     """
     Tests for the VerificationDeadline model.
diff --git a/lms/djangoapps/verify_student/tests/test_services.py b/lms/djangoapps/verify_student/tests/test_services.py
index 7c279533472..3aaaa2774f5 100644
--- a/lms/djangoapps/verify_student/tests/test_services.py
+++ b/lms/djangoapps/verify_student/tests/test_services.py
@@ -16,7 +16,7 @@ from nose.tools import (
 )
 
 from common.test.utils import MockS3Mixin
-from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
+from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
 from lms.djangoapps.verify_student.services import IDVerificationService
 from student.tests.factories import UserFactory
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@@ -34,52 +34,6 @@ class TestIDVerificationService(MockS3Mixin, ModuleStoreTestCase):
     Tests for IDVerificationService.
     """
 
-    def test_active_for_user(self):
-        """
-        Make sure we can retrive a user's active (in progress) verification
-        attempt.
-        """
-        user = UserFactory.create()
-
-        # This user has no active at the moment...
-        assert_is_none(IDVerificationService.active_for_user(user))
-
-        # Create an attempt and mark it ready...
-        attempt = SoftwareSecurePhotoVerification(user=user)
-        attempt.mark_ready()
-        assert_equals(attempt, IDVerificationService.active_for_user(user))
-
-        # A new user won't see this...
-        user2 = UserFactory.create()
-        user2.save()
-        assert_is_none(IDVerificationService.active_for_user(user2))
-
-        # If it's got a different status, it doesn't count
-        for status in ["submitted", "must_retry", "approved", "denied"]:
-            attempt.status = status
-            attempt.save()
-            assert_is_none(IDVerificationService.active_for_user(user))
-
-        # But if we create yet another one and mark it ready, it passes again.
-        attempt_2 = SoftwareSecurePhotoVerification(user=user)
-        attempt_2.mark_ready()
-        assert_equals(attempt_2, IDVerificationService.active_for_user(user))
-
-        # And if we add yet another one with a later created time, we get that
-        # one instead. We always want the most recent attempt marked ready()
-        attempt_3 = SoftwareSecurePhotoVerification(
-            user=user,
-            created_at=attempt_2.created_at + timedelta(days=1)
-        )
-        attempt_3.save()
-
-        # We haven't marked attempt_3 ready yet, so attempt_2 still wins
-        assert_equals(attempt_2, IDVerificationService.active_for_user(user))
-
-        # Now we mark attempt_3 ready and expect it to come back
-        attempt_3.mark_ready()
-        assert_equals(attempt_3, IDVerificationService.active_for_user(user))
-
     def test_user_is_verified(self):
         """
         Test to make sure we correctly answer whether a user has been verified.
@@ -123,26 +77,31 @@ class TestIDVerificationService(MockS3Mixin, ModuleStoreTestCase):
         # test for correct status when no error returned
         user = UserFactory.create()
         status = IDVerificationService.user_status(user)
-        self.assertEquals(status, ('none', ''))
+        self.assertEquals(status, {'status': 'none', 'error': '', 'should_display': True})
 
-        # test for when one has been created
-        attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
+        # test for when photo verification has been created
+        SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
         status = IDVerificationService.user_status(user)
-        self.assertEquals(status, ('approved', ''))
+        self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': True})
 
-        # create another one for the same user, make sure the right one is
-        # returned
+        # create another photo verification for the same user, make sure the denial
+        # is handled properly
         SoftwareSecurePhotoVerification.objects.create(
             user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
         )
         status = IDVerificationService.user_status(user)
-        self.assertEquals(status, ('approved', ''))
+        self.assertEquals(status, {'status': 'must_reverify', 'error': ['id_image_missing'], 'should_display': True})
+
+        # test for when sso verification has been created
+        SSOVerification.objects.create(user=user, status='approved')
+        status = IDVerificationService.user_status(user)
+        self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': False})
 
-        # now delete the first one and verify that the denial is being handled
-        # properly
-        attempt.delete()
+        # create another sso verification for the same user, make sure the denial
+        # is handled properly
+        SSOVerification.objects.create(user=user, status='denied')
         status = IDVerificationService.user_status(user)
-        self.assertEquals(status, ('must_reverify', ['id_image_missing']))
+        self.assertEquals(status, {'status': 'must_reverify', 'error': '', 'should_display': False})
 
     @ddt.unpack
     @ddt.data(
diff --git a/lms/djangoapps/verify_student/tests/test_utils.py b/lms/djangoapps/verify_student/tests/test_utils.py
index 17c2041dbb2..7ce101600c8 100644
--- a/lms/djangoapps/verify_student/tests/test_utils.py
+++ b/lms/djangoapps/verify_student/tests/test_utils.py
@@ -5,13 +5,14 @@ Tests for verify_student utility functions.
 
 from datetime import datetime, timedelta
 
+import ddt
 import unittest
 import pytz
 from mock import patch
 from pytest import mark
 from django.conf import settings
-from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
-from lms.djangoapps.verify_student.utils import verification_for_datetime
+from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
+from lms.djangoapps.verify_student.utils import verification_for_datetime, most_recent_verification
 from student.tests.factories import UserFactory
 
 FAKE_SETTINGS = {
@@ -19,6 +20,7 @@ FAKE_SETTINGS = {
 }
 
 
+@ddt.ddt
 @patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
 @mark.django_db
 class TestVerifyStudentUtils(unittest.TestCase):
@@ -83,3 +85,46 @@ class TestVerifyStudentUtils(unittest.TestCase):
         query = SoftwareSecurePhotoVerification.objects.filter(user=user)
         result = verification_for_datetime(deadline, query)
         self.assertEqual(result, second_attempt)
+
+    @ddt.data(
+        (False, False, None, None),
+        (True, False, None, 'photo'),
+        (False, True, None, 'sso'),
+        (True, True, 'photo', 'sso'),
+        (True, True, 'sso', 'photo'),
+    )
+    @ddt.unpack
+    def test_most_recent_verification(
+            self,
+            create_photo_verification,
+            create_sso_verification,
+            first_verification,
+            expected_verification):
+        user = UserFactory.create()
+        photo_verification = None
+        sso_verification = None
+
+        if not first_verification:
+            if create_photo_verification:
+                photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
+            if create_sso_verification:
+                sso_verification = SSOVerification.objects.create(user=user)
+        elif first_verification == 'photo':
+            photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
+            sso_verification = SSOVerification.objects.create(user=user)
+        else:
+            sso_verification = SSOVerification.objects.create(user=user)
+            photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
+
+        most_recent = most_recent_verification(
+            SoftwareSecurePhotoVerification.objects.all(),
+            SSOVerification.objects.all(),
+            'created_at'
+        )
+
+        if not expected_verification:
+            self.assertEqual(most_recent, None)
+        elif expected_verification == 'photo':
+            self.assertEqual(most_recent, photo_verification)
+        else:
+            self.assertEqual(most_recent, sso_verification)
diff --git a/lms/djangoapps/verify_student/utils.py b/lms/djangoapps/verify_student/utils.py
index 943f761ce90..f6f8c1b0411 100644
--- a/lms/djangoapps/verify_student/utils.py
+++ b/lms/djangoapps/verify_student/utils.py
@@ -95,3 +95,30 @@ def send_verification_status_email(context):
                 subject=context['subject'],
                 email=context['email']
             ))
+
+
+def most_recent_verification(photo_id_verifications, sso_id_verifications, most_recent_key):
+    """
+    Return the most recent verification given querysets for both photo and sso verifications.
+
+    Arguments:
+            photo_id_verifications: Queryset containing photo verifications
+            sso_id_verifications: Queryset containing sso verifications
+            most_recent_key: Either 'updated_at' or 'created_at'
+
+    Returns:
+        The most recent verification.
+    """
+    photo_id_verification = photo_id_verifications and photo_id_verifications.first()
+    sso_id_verification = sso_id_verifications and sso_id_verifications.first()
+
+    if not photo_id_verification and not sso_id_verification:
+        return None
+    elif photo_id_verification and not sso_id_verification:
+        return photo_id_verification
+    elif sso_id_verification and not photo_id_verification:
+        return sso_id_verification
+    elif getattr(photo_id_verification, most_recent_key) > getattr(sso_id_verification, most_recent_key):
+        return photo_id_verification
+    else:
+        return sso_id_verification
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index fb7886fa128..12411264c0c 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -646,11 +646,13 @@ class PayAndVerifyView(View):
         Returns:
             datetime object in string format
         """
-        photo_verifications = IDVerificationService.verification_valid_or_pending(user)
+        expiration_datetime = IDVerificationService.get_expiration_datetime(
+            user, ['submitted', 'approved', 'must_retry']
+        )
         # return 'expiration_datetime' of latest photo verification if found,
         # otherwise implicitly return ''
-        if photo_verifications:
-            return photo_verifications[0].expiration_datetime.strftime(date_format)
+        if expiration_datetime:
+            return expiration_datetime.strftime(date_format)
 
         return ''
 
@@ -1226,9 +1228,9 @@ class ReverifyView(View):
         Most of the work is done client-side by composing the same
         Backbone views used in the initial verification flow.
         """
-        status, __ = IDVerificationService.user_status(request.user)
+        verification_status = IDVerificationService.user_status(request.user)
 
-        expiration_datetime = IDVerificationService.get_expiration_datetime(request.user)
+        expiration_datetime = IDVerificationService.get_expiration_datetime(request.user, ['approved'])
         can_reverify = False
         if expiration_datetime:
             if is_verification_expiring_soon(expiration_datetime):
@@ -1243,7 +1245,7 @@ class ReverifyView(View):
         # A photo verification is marked as 'pending' if its status is either
         # 'submitted' or 'must_retry'.
 
-        if status in ["none", "must_reverify", "expired", "pending"] or can_reverify:
+        if verification_status['status'] in ["none", "must_reverify", "expired", "pending"] or can_reverify:
             context = {
                 "user_full_name": request.user.profile.name,
                 "platform_name": configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
@@ -1252,6 +1254,6 @@ class ReverifyView(View):
             return render_to_response("verify_student/reverify.html", context)
         else:
             context = {
-                "status": status
+                "status": verification_status['status']
             }
             return render_to_response("verify_student/reverify_not_allowed.html", context)
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index ca552505779..ff488593506 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -399,7 +399,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
           <%include file="_dashboard_show_consent.html" args="course_overview=course_overview, course_target=course_target, enrollment=enrollment, enterprise_customer_name=enterprise_customer_name"/>
         %endif
 
-        % if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_RESUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY]:
+        % if verification_status.get('should_display') and verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_RESUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY]:
           <div class="message message-status wrapper-message-primary is-shown">
             % if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
               <div class="verification-reminder">
diff --git a/lms/templates/dashboard/_dashboard_status_verification.html b/lms/templates/dashboard/_dashboard_status_verification.html
index 2dce1a12aff..8ed8899a8f6 100644
--- a/lms/templates/dashboard/_dashboard_status_verification.html
+++ b/lms/templates/dashboard/_dashboard_status_verification.html
@@ -5,42 +5,44 @@ from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
 %>
 
-%if verification_status == 'approved':
-    <li class="status status-verification is-accepted">
-        <span class="title status-title">${_("Current Verification Status: Approved")}</span>
-        <p class="status-note">${_("Your edX verification has been approved. Your verification is effective for one year after submission.")}</p>
-    </li>
-%elif verification_status == 'pending':
-    <li class="status status-verification is-pending">
-        <span class="title status-title">${_("Current Verification Status: Pending")}</span>
-        <p class="status-note">${_("Your edX ID verification is pending. Your verification information has been submitted and will be reviewed shortly.")}</p>
-    </li>
-%elif verification_status in ['denied','must_reverify', 'must_retry']:
-    <li class="status status-verification is-denied">
-        <span class="title status-title">${_("Current Verification Status: Denied")}</span>
-        <p class="status-note">
-      ${_("Your verification submission was not accepted. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}
+%if verification_display:
+    %if verification_status == 'approved':
+        <li class="status status-verification is-accepted">
+            <span class="title status-title">${_("Current Verification Status: Approved")}</span>
+            <p class="status-note">${_("Your edX verification has been approved. Your verification is effective for one year after submission.")}</p>
+        </li>
+    %elif verification_status == 'pending':
+        <li class="status status-verification is-pending">
+            <span class="title status-title">${_("Current Verification Status: Pending")}</span>
+            <p class="status-note">${_("Your edX ID verification is pending. Your verification information has been submitted and will be reviewed shortly.")}</p>
+        </li>
+    %elif verification_status in ['denied','must_reverify', 'must_retry']:
+        <li class="status status-verification is-denied">
+            <span class="title status-title">${_("Current Verification Status: Denied")}</span>
+            <p class="status-note">
+        ${_("Your verification submission was not accepted. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}
 
-          %if verification_errors:
-            <br><br>
-            ${_("Your verification was denied for the following reasons:")}<br>
-            <ul>
-              %for error in verification_errors:
-                <li>${error}</li>
-              %endfor
-            </ul>
-          %endif
-        </p>
-      <div class="btn-reverify">
-        <a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
-      </div>
-    </li>
-%elif verification_status == 'expired':
-    <li class="status status-verification is-denied">
-        <span class="title status-title">${_("Current Verification Status: Expired")}</span>
-        <p class="status-note">${_("Your verification has expired. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}</p>
+            %if verification_errors:
+                <br><br>
+                ${_("Your verification was denied for the following reasons:")}<br>
+                <ul>
+                %for error in verification_errors:
+                    <li>${error}</li>
+                %endfor
+                </ul>
+            %endif
+            </p>
         <div class="btn-reverify">
             <a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
         </div>
-    </li>
+        </li>
+    %elif verification_status == 'expired':
+        <li class="status status-verification is-denied">
+            <span class="title status-title">${_("Current Verification Status: Expired")}</span>
+            <p class="status-note">${_("Your verification has expired. To receive a verified certificate, you must submit a new photo of yourself and your government-issued photo ID before the verification deadline for your course.")}</p>
+            <div class="btn-reverify">
+                <a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
+            </div>
+        </li>
+    %endif
 %endif
diff --git a/openedx/core/djangoapps/user_api/serializers.py b/openedx/core/djangoapps/user_api/serializers.py
index c4dcf4fa3bd..7b24ab35b50 100644
--- a/openedx/core/djangoapps/user_api/serializers.py
+++ b/openedx/core/djangoapps/user_api/serializers.py
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
 from django.utils.timezone import now
 from rest_framework import serializers
 
-from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
+from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
 
 from .models import UserPreference
 
@@ -94,9 +94,9 @@ class CountryTimeZoneSerializer(serializers.Serializer):  # pylint: disable=abst
     description = serializers.CharField()
 
 
-class SoftwareSecurePhotoVerificationSerializer(serializers.ModelSerializer):
+class IDVerificationSerializer(serializers.ModelSerializer):
     """
-    Serializer that generates a representation of a user's photo verification status.
+    Serializer that generates a representation of a user's ID verification status.
     """
     is_verified = serializers.SerializerMethodField()
 
@@ -106,6 +106,16 @@ class SoftwareSecurePhotoVerificationSerializer(serializers.ModelSerializer):
         """
         return obj.status == 'approved' and obj.expiration_datetime > now()
 
+
+class SoftwareSecurePhotoVerificationSerializer(IDVerificationSerializer):
+
     class Meta(object):
         fields = ('status', 'expiration_datetime', 'is_verified')
         model = SoftwareSecurePhotoVerification
+
+
+class SSOVerificationSerializer(IDVerificationSerializer):
+
+    class Meta(object):
+        fields = ('status', 'expiration_datetime', 'is_verified')
+        model = SSOVerification
diff --git a/openedx/core/djangoapps/user_api/urls.py b/openedx/core/djangoapps/user_api/urls.py
index 9ea27ec0776..2ac78c660dc 100644
--- a/openedx/core/djangoapps/user_api/urls.py
+++ b/openedx/core/djangoapps/user_api/urls.py
@@ -14,7 +14,7 @@ from .accounts.views import (
     DeactivateLogoutView
 )
 from .preferences.views import PreferencesDetailView, PreferencesView
-from .verification_api.views import PhotoVerificationStatusView
+from .verification_api.views import IDVerificationStatusView
 from .validation.views import RegistrationValidationView
 
 ME = AccountViewSet.as_view({
@@ -81,7 +81,7 @@ urlpatterns = [
     ),
     url(
         r'^v1/accounts/{}/verification_status/$'.format(settings.USERNAME_PATTERN),
-        PhotoVerificationStatusView.as_view(),
+        IDVerificationStatusView.as_view(),
         name='verification_status'
     ),
     url(
diff --git a/openedx/core/djangoapps/user_api/verification_api/views.py b/openedx/core/djangoapps/user_api/verification_api/views.py
index 5244915f8e3..eae82f74e11 100644
--- a/openedx/core/djangoapps/user_api/verification_api/views.py
+++ b/openedx/core/djangoapps/user_api/verification_api/views.py
@@ -5,23 +5,38 @@ from rest_framework.authentication import SessionAuthentication
 from rest_framework.generics import RetrieveAPIView
 from rest_framework_oauth.authentication import OAuth2Authentication
 
-from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
-from openedx.core.djangoapps.user_api.serializers import SoftwareSecurePhotoVerificationSerializer
+from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
+from lms.djangoapps.verify_student.utils import most_recent_verification
+from openedx.core.djangoapps.user_api.serializers import (
+    SoftwareSecurePhotoVerificationSerializer, SSOVerificationSerializer,
+)
 from openedx.core.lib.api.permissions import IsStaffOrOwner
 
 
-class PhotoVerificationStatusView(RetrieveAPIView):
-    """ PhotoVerificationStatus detail endpoint. """
+class IDVerificationStatusView(RetrieveAPIView):
+    """ IDVerificationStatus detail endpoint. """
     authentication_classes = (JwtAuthentication, OAuth2Authentication, SessionAuthentication,)
     permission_classes = (IsStaffOrOwner,)
-    serializer_class = SoftwareSecurePhotoVerificationSerializer
+
+    def get_serializer(self, *args, **kwargs):
+        """
+        Overrides default get_serializer in order to choose the correct serializer for the instance.
+        """
+        instance = args[0]
+        kwargs['context'] = self.get_serializer_context()
+        if isinstance(instance, SoftwareSecurePhotoVerification):
+            return SoftwareSecurePhotoVerificationSerializer(*args, **kwargs)
+        else:
+            return SSOVerificationSerializer(*args, **kwargs)
 
     def get_object(self):
         username = self.kwargs['username']
-        verifications = SoftwareSecurePhotoVerification.objects.filter(user__username=username).order_by('-updated_at')
+        photo_verifications = SoftwareSecurePhotoVerification.objects.filter(
+            user__username=username).order_by('-updated_at')
+        sso_verifications = SSOVerification.objects.filter(user__username=username).order_by('-updated_at')
 
-        if len(verifications) > 0:
-            verification = verifications[0]
+        if photo_verifications or sso_verifications:
+            verification = most_recent_verification(photo_verifications, sso_verifications, 'updated_at')
             self.check_object_permissions(self.request, verification)
             return verification
 
-- 
GitLab