Partial revert of "ENT-945 Update IDVerification interfaces to accountfor SSOVerification"
This partially reverts commit ee1c3a4548.
The migration files introduced by the commit have been kept since they have been run
already on several enviornments.
This commit is contained in:
@@ -154,12 +154,9 @@ 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
|
||||
@@ -217,8 +214,7 @@ def check_verify_status_by_course(user, course_enrollments):
|
||||
|
||||
status_by_course[enrollment.course_id] = {
|
||||
'status': status,
|
||||
'days_until_deadline': days_until_deadline,
|
||||
'should_display': should_display,
|
||||
'days_until_deadline': days_until_deadline
|
||||
}
|
||||
|
||||
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 = IDVerificationService.user_status(user)
|
||||
verification_errors = get_verification_error_reasons_for_display(verification_status['error'])
|
||||
verification_status, verification_error_codes = IDVerificationService.user_status(user)
|
||||
verification_errors = get_verification_error_reasons_for_display(verification_error_codes)
|
||||
|
||||
# Gets data for midcourse reverifications, if any are necessary or have failed
|
||||
statuses = ["approved", "denied", "pending", "must_reverify"]
|
||||
@@ -770,9 +770,7 @@ 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['status'] in valid_verification_statuses and
|
||||
verification_status['should_display']))
|
||||
display_sidebar_on_dashboard = len(order_history_list) or verification_status in valid_verification_statuses
|
||||
|
||||
# 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]:
|
||||
@@ -804,8 +802,7 @@ def student_dashboard(request):
|
||||
'credit_statuses': _credit_statuses(user, course_enrollments),
|
||||
'show_email_settings_for': show_email_settings_for,
|
||||
'reverifications': reverifications,
|
||||
'verification_display': verification_status['should_display'],
|
||||
'verification_status': verification_status['status'],
|
||||
'verification_status': verification_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['status'],
|
||||
'verification_status': verification_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']
|
||||
status=expected_verification_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,17 +256,12 @@ 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': unicode(expected_verification_status),
|
||||
'expected_verification_status': SoftwareSecurePhotoVerification.STATUS.approved
|
||||
}
|
||||
)
|
||||
|
||||
@@ -282,17 +277,12 @@ 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': unicode(expected_verification_status),
|
||||
'expected_verification_status': SoftwareSecurePhotoVerification.STATUS.approved
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -45,22 +45,13 @@ 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': expected_verification_status,
|
||||
'expected_verification_status': 'approved'
|
||||
}
|
||||
|
||||
user_status_mock.side_effect = [
|
||||
{'status': 'pending', 'error': '', 'should_display': True},
|
||||
{'status': 'approved', 'error': '', 'should_display': True}
|
||||
]
|
||||
user_status_mock.side_effect = [('pending', ''), ('approved', '')]
|
||||
|
||||
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.user_has_valid_or_pending(request.user),
|
||||
'verified': IDVerificationService.verification_valid_or_pending(request.user).exists(),
|
||||
'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."""
|
||||
verification_status = IDVerificationService.user_status(self.user)
|
||||
return verification_status['status']
|
||||
status, _ = IDVerificationService.user_status(self.user)
|
||||
return status
|
||||
|
||||
def must_retry(self):
|
||||
"""Return True if the user must re-submit verification, False otherwise."""
|
||||
|
||||
@@ -7,25 +7,12 @@ from django.db import migrations
|
||||
|
||||
|
||||
def populate_id_verification_aggregate(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
IDVerificationAggregate = apps.get_model('verify_student', 'IDVerificationAggregate')
|
||||
SoftwareSecurePhotoVerification = apps.get_model('verify_student', 'SoftwareSecurePhotoVerification')
|
||||
SSOVerification = apps.get_model('verify_student', 'SSOVerification')
|
||||
|
||||
software_secure_verifications = SoftwareSecurePhotoVerification.objects.all()
|
||||
sso_verifications = SSOVerification.objects.all()
|
||||
for verification in chain(software_secure_verifications, sso_verifications):
|
||||
content_type = ContentType.objects.get_for_model(verification)
|
||||
IDVerificationAggregate.objects.create(
|
||||
status=verification.status,
|
||||
user=verification.user,
|
||||
name=verification.name,
|
||||
created_at=verification.created_at,
|
||||
updated_at=verification.updated_at,
|
||||
content_type=content_type,
|
||||
object_id=verification.id,
|
||||
)
|
||||
|
||||
"""
|
||||
The code from this migration was removed because it caused a spike in database errors
|
||||
when it was run in the edX production environment. More details can be found here:
|
||||
https://openedx.atlassian.net/browse/ENT-969
|
||||
"""
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
|
||||
@@ -120,26 +120,6 @@ 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
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
|
||||
class IDVerificationAggregate(IDVerificationAttempt):
|
||||
"""
|
||||
@@ -166,27 +146,6 @@ class IDVerificationAggregate(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 self.content_object.should_display_status_to_user()
|
||||
|
||||
|
||||
def post_save_id_verification(sender, instance, created, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Post save handler to create/update IDVerificationAttempt instances.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(instance)
|
||||
try:
|
||||
id_verification = IDVerificationAggregate.objects.get(content_type=content_type, object_id=instance.id)
|
||||
except IDVerificationAggregate.DoesNotExist:
|
||||
id_verification = IDVerificationAggregate(content_type=content_type, object_id=instance.id)
|
||||
id_verification.status = instance.status
|
||||
id_verification.user = instance.user
|
||||
id_verification.name = instance.name
|
||||
id_verification.created_at = instance.created_at
|
||||
id_verification.updated_at = instance.updated_at
|
||||
id_verification.save()
|
||||
|
||||
|
||||
class SSOVerification(IDVerificationAttempt):
|
||||
"""
|
||||
@@ -229,12 +188,6 @@ 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
|
||||
|
||||
models.signals.post_save.connect(post_save_id_verification, sender=SSOVerification)
|
||||
|
||||
|
||||
class PhotoVerification(IDVerificationAttempt, DeletableByUserValue):
|
||||
"""
|
||||
@@ -328,6 +281,22 @@ 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
|
||||
@@ -880,12 +849,6 @@ 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
|
||||
|
||||
models.signals.post_save.connect(post_save_id_verification, sender=SoftwareSecurePhotoVerification)
|
||||
|
||||
|
||||
class VerificationDeadline(TimeStampedModel):
|
||||
"""
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 IDVerificationAggregate
|
||||
from .models import SoftwareSecurePhotoVerification
|
||||
from .utils import earliest_allowed_verification_date
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -70,7 +70,7 @@ class IDVerificationService(object):
|
||||
that are still valid according to the earliest_allowed_date
|
||||
value or policy settings.
|
||||
"""
|
||||
return IDVerificationAggregate.objects.filter(
|
||||
return SoftwareSecurePhotoVerification.objects.filter(
|
||||
status="approved",
|
||||
created_at__gte=(earliest_allowed_date or earliest_allowed_verification_date()),
|
||||
)
|
||||
@@ -80,7 +80,7 @@ class IDVerificationService(object):
|
||||
"""
|
||||
Return a query set for all records associated with the given user.
|
||||
"""
|
||||
return IDVerificationAggregate.objects.filter(user=user)
|
||||
return SoftwareSecurePhotoVerification.objects.filter(user=user)
|
||||
|
||||
@classmethod
|
||||
def get_verified_users(cls, users):
|
||||
@@ -115,7 +115,7 @@ class IDVerificationService(object):
|
||||
valid_statuses = ['submitted', 'approved', 'must_retry']
|
||||
|
||||
if queryset is None:
|
||||
queryset = IDVerificationAggregate.objects.filter(user=user)
|
||||
queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
|
||||
|
||||
return queryset.filter(
|
||||
status__in=valid_statuses,
|
||||
@@ -141,11 +141,11 @@ class IDVerificationService(object):
|
||||
verification.
|
||||
"""
|
||||
if queryset is None:
|
||||
queryset = IDVerificationAggregate.objects.filter(user=user)
|
||||
queryset = SoftwareSecurePhotoVerification.objects.filter(user=user)
|
||||
|
||||
id_verification = queryset.filter(status='approved').first()
|
||||
if id_verification:
|
||||
return id_verification.expiration_datetime
|
||||
photo_verification = queryset.filter(status='approved').first()
|
||||
if photo_verification:
|
||||
return photo_verification.expiration_datetime
|
||||
|
||||
@classmethod
|
||||
def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, queryset=None):
|
||||
@@ -157,6 +157,26 @@ class IDVerificationService(object):
|
||||
"""
|
||||
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')
|
||||
|
||||
if active_attempts:
|
||||
return active_attempts[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def user_status(cls, user):
|
||||
"""
|
||||
@@ -168,51 +188,46 @@ 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 most recent verification
|
||||
This checks initial verifications
|
||||
"""
|
||||
# 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,
|
||||
}
|
||||
status = 'none'
|
||||
error_msg = ''
|
||||
|
||||
# We need to check the user's most recent attempt.
|
||||
try:
|
||||
attempts = IDVerificationAggregate.objects.filter(user=user).order_by('-updated_at')
|
||||
attempt = attempts[0].content_object
|
||||
except IndexError:
|
||||
# The user has no verification attempts, return the default set of data.
|
||||
return user_status
|
||||
if cls.user_is_verified(user):
|
||||
status = 'approved'
|
||||
|
||||
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']:
|
||||
elif cls.user_has_valid_or_pending(user):
|
||||
# user_has_valid_or_pending does include 'approved', but if we are
|
||||
# here, we know that the attempt is still pending
|
||||
user_status['status'] = 'pending'
|
||||
status = 'pending'
|
||||
|
||||
return user_status
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def verification_status_for_user(cls, user, user_enrollment_mode, user_is_verified=None):
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -22,7 +21,6 @@ from testfixtures import LogCapture
|
||||
from common.test.utils import MockS3Mixin
|
||||
from lms.djangoapps.verify_student.models import (
|
||||
SoftwareSecurePhotoVerification,
|
||||
SSOVerification,
|
||||
VerificationDeadline,
|
||||
VerificationException
|
||||
)
|
||||
@@ -97,38 +95,11 @@ 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(TestVerification, MockS3Mixin, ModuleStoreTestCase):
|
||||
class TestPhotoVerification(MockS3Mixin, ModuleStoreTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPhotoVerification, self).setUp()
|
||||
@@ -281,7 +252,24 @@ class TestPhotoVerification(TestVerification, MockS3Mixin, ModuleStoreTestCase):
|
||||
def test_active_at_datetime(self):
|
||||
user = UserFactory.create()
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=user)
|
||||
self.verification_active_at_datetime(attempt)
|
||||
|
||||
# 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)))
|
||||
|
||||
def test_initial_verification_for_user(self):
|
||||
"""Test that method 'get_initial_verification' of model
|
||||
@@ -352,16 +340,6 @@ class TestPhotoVerification(TestVerification, 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.
|
||||
|
||||
@@ -16,7 +16,7 @@ from nose.tools import (
|
||||
)
|
||||
|
||||
from common.test.utils import MockS3Mixin
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -34,6 +34,52 @@ 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.
|
||||
@@ -77,31 +123,26 @@ class TestIDVerificationService(MockS3Mixin, ModuleStoreTestCase):
|
||||
# test for correct status when no error returned
|
||||
user = UserFactory.create()
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, {'status': 'none', 'error': '', 'should_display': True})
|
||||
self.assertEquals(status, ('none', ''))
|
||||
|
||||
# test for when photo verification has been created
|
||||
SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
|
||||
# test for when one has been created
|
||||
attempt = SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': True})
|
||||
self.assertEquals(status, ('approved', ''))
|
||||
|
||||
# create another photo verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
# create another one for the same user, make sure the right one is
|
||||
# returned
|
||||
SoftwareSecurePhotoVerification.objects.create(
|
||||
user=user, status='denied', error_msg='[{"photoIdReasons": ["Not provided"]}]'
|
||||
)
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, {'status': 'must_reverify', 'error': ['id_image_missing'], 'should_display': True})
|
||||
self.assertEquals(status, ('approved', ''))
|
||||
|
||||
# test for when sso verification has been created
|
||||
SSOVerification.objects.create(user=user, status='approved')
|
||||
# now delete the first one and verify that the denial is being handled
|
||||
# properly
|
||||
attempt.delete()
|
||||
status = IDVerificationService.user_status(user)
|
||||
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})
|
||||
self.assertEquals(status, ('must_reverify', ['id_image_missing']))
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
|
||||
@@ -1226,7 +1226,7 @@ class ReverifyView(View):
|
||||
Most of the work is done client-side by composing the same
|
||||
Backbone views used in the initial verification flow.
|
||||
"""
|
||||
verification_status = IDVerificationService.user_status(request.user)
|
||||
status, __ = IDVerificationService.user_status(request.user)
|
||||
|
||||
expiration_datetime = IDVerificationService.get_expiration_datetime(request.user)
|
||||
can_reverify = False
|
||||
@@ -1243,7 +1243,7 @@ class ReverifyView(View):
|
||||
# A photo verification is marked as 'pending' if its status is either
|
||||
# 'submitted' or 'must_retry'.
|
||||
|
||||
if verification_status['status'] in ["none", "must_reverify", "expired", "pending"] or can_reverify:
|
||||
if 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 +1252,6 @@ class ReverifyView(View):
|
||||
return render_to_response("verify_student/reverify.html", context)
|
||||
else:
|
||||
context = {
|
||||
"status": verification_status['status']
|
||||
"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('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]:
|
||||
% 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]:
|
||||
<div class="message message-status wrapper-message-primary is-shown">
|
||||
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
|
||||
<div class="verification-reminder">
|
||||
|
||||
@@ -5,44 +5,42 @@ from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
%>
|
||||
|
||||
%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_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>
|
||||
%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>
|
||||
<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>
|
||||
<div class="btn-reverify">
|
||||
<a href="${reverse('verify_student_reverify')}" class="action action-reverify">${_("Resubmit Verification")}</a>
|
||||
</div>
|
||||
</li>
|
||||
%endif
|
||||
</li>
|
||||
%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 IDVerificationAggregate
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from .models import UserPreference
|
||||
|
||||
@@ -94,9 +94,9 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst
|
||||
description = serializers.CharField()
|
||||
|
||||
|
||||
class IDVerificationAggregateSerializer(serializers.ModelSerializer):
|
||||
class SoftwareSecurePhotoVerificationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer that generates a representation of a user's ID verification status.
|
||||
Serializer that generates a representation of a user's photo verification status.
|
||||
"""
|
||||
is_verified = serializers.SerializerMethodField()
|
||||
|
||||
@@ -108,4 +108,4 @@ class IDVerificationAggregateSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta(object):
|
||||
fields = ('status', 'expiration_datetime', 'is_verified')
|
||||
model = IDVerificationAggregate
|
||||
model = SoftwareSecurePhotoVerification
|
||||
|
||||
@@ -14,7 +14,7 @@ from .accounts.views import (
|
||||
DeactivateLogoutView
|
||||
)
|
||||
from .preferences.views import PreferencesDetailView, PreferencesView
|
||||
from .verification_api.views import IDVerificationStatusView
|
||||
from .verification_api.views import PhotoVerificationStatusView
|
||||
from .validation.views import RegistrationValidationView
|
||||
|
||||
ME = AccountViewSet.as_view({
|
||||
@@ -81,7 +81,7 @@ urlpatterns = [
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/verification_status/$'.format(settings.USERNAME_PATTERN),
|
||||
IDVerificationStatusView.as_view(),
|
||||
PhotoVerificationStatusView.as_view(),
|
||||
name='verification_status'
|
||||
),
|
||||
url(
|
||||
|
||||
@@ -5,20 +5,20 @@ 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 IDVerificationAggregate
|
||||
from openedx.core.djangoapps.user_api.serializers import IDVerificationAggregateSerializer
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
from openedx.core.djangoapps.user_api.serializers import SoftwareSecurePhotoVerificationSerializer
|
||||
from openedx.core.lib.api.permissions import IsStaffOrOwner
|
||||
|
||||
|
||||
class IDVerificationStatusView(RetrieveAPIView):
|
||||
""" IDVerificationStatus detail endpoint. """
|
||||
class PhotoVerificationStatusView(RetrieveAPIView):
|
||||
""" PhotoVerificationStatus detail endpoint. """
|
||||
authentication_classes = (JwtAuthentication, OAuth2Authentication, SessionAuthentication,)
|
||||
permission_classes = (IsStaffOrOwner,)
|
||||
serializer_class = IDVerificationAggregateSerializer
|
||||
serializer_class = SoftwareSecurePhotoVerificationSerializer
|
||||
|
||||
def get_object(self):
|
||||
username = self.kwargs['username']
|
||||
verifications = IDVerificationAggregate.objects.filter(user__username=username).order_by('-updated_at')
|
||||
verifications = SoftwareSecurePhotoVerification.objects.filter(user__username=username).order_by('-updated_at')
|
||||
|
||||
if len(verifications) > 0:
|
||||
verification = verifications[0]
|
||||
|
||||
Reference in New Issue
Block a user