Merge pull request #18070 from edx/bexline/sso_id_verification2
ENT-945 Using SSOVerifications in ID Verification flow
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
|
||||
@@ -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']
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
"""
|
||||
@@ -281,22 +279,6 @@ class PhotoVerification(IDVerificationAttempt):
|
||||
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
|
||||
@@ -873,6 +855,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):
|
||||
"""
|
||||
|
||||
@@ -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 the list of users that have non-expired verifications of either type from
|
||||
the given list of users.
|
||||
"""
|
||||
return cls.verified_query().filter(user__in=users).select_related('user')
|
||||
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 verification_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
|
||||
def get_expiration_datetime(cls, user, statuses):
|
||||
"""
|
||||
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')
|
||||
|
||||
@classmethod
|
||||
def get_expiration_datetime(cls, user, queryset=None):
|
||||
"""
|
||||
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_verification = queryset.filter(status='approved').first()
|
||||
if photo_verification:
|
||||
return photo_verification.expiration_datetime
|
||||
photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
|
||||
sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
|
||||
|
||||
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()
|
||||
filter_kwargs = {
|
||||
'user': user,
|
||||
'status__in': ['submitted', 'approved', 'must_retry'],
|
||||
'created_at__gte': earliest_allowed_verification_date()
|
||||
}
|
||||
|
||||
@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')
|
||||
|
||||
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
|
||||
This checks most recent verification
|
||||
"""
|
||||
status = 'none'
|
||||
error_msg = ''
|
||||
# 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,
|
||||
}
|
||||
|
||||
if cls.user_is_verified(user):
|
||||
status = 'approved'
|
||||
# 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')
|
||||
|
||||
elif cls.user_has_valid_or_pending(user):
|
||||
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 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 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'
|
||||
user_status['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()
|
||||
|
||||
return (status, error_msg)
|
||||
return user_status
|
||||
|
||||
@classmethod
|
||||
def verification_status_for_user(cls, user, user_enrollment_mode, user_is_verified=None):
|
||||
|
||||
@@ -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
|
||||
@@ -364,6 +376,16 @@ class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
|
||||
self.assertFalse(attempt.retire_user(user_id=47))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -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})
|
||||
|
||||
# now delete the first one and verify that the denial is being handled
|
||||
# properly
|
||||
attempt.delete()
|
||||
# test for when sso verification has been created
|
||||
SSOVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, ('must_reverify', ['id_image_missing']))
|
||||
self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': False})
|
||||
|
||||
# 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, {'status': 'must_reverify', 'error': '', 'should_display': False})
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user