Merge pull request #18337 from edx/iahmad/ENT-1017-Verified-certificates-for-bank-learners
incorporated manual verification
This commit is contained in:
@@ -409,7 +409,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
|
||||
|
||||
RequestCache.clear_request_cache()
|
||||
|
||||
expected_query_count = 42
|
||||
expected_query_count = 43
|
||||
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
|
||||
with check_mongo_calls(mongo_count):
|
||||
with self.assertNumQueries(expected_query_count):
|
||||
@@ -2151,7 +2151,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
'failed': 3,
|
||||
'skipped': 2
|
||||
}
|
||||
with self.assertNumQueries(114):
|
||||
with self.assertNumQueries(122):
|
||||
self.assertCertificatesGenerated(task_input, expected_results)
|
||||
|
||||
expected_results = {
|
||||
|
||||
@@ -5,7 +5,7 @@ Admin site configurations for verify_student.
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification, ManualVerification
|
||||
|
||||
|
||||
@admin.register(SoftwareSecurePhotoVerification)
|
||||
@@ -27,3 +27,13 @@ class SSOVerificationAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('user', 'identity_provider_slug', 'identity_provider_type',)
|
||||
raw_id_fields = ('user',)
|
||||
search_fields = ('user__username', 'identity_provider_slug',)
|
||||
|
||||
|
||||
@admin.register(ManualVerification)
|
||||
class ManualVerificationAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin for the ManualVerification table.
|
||||
"""
|
||||
list_display = ('id', 'user', 'status', 'reason', 'created_at', 'updated_at',)
|
||||
raw_id_fields = ('user',)
|
||||
search_fields = ('user__username', 'reason',)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Django admin commands related to verify_student
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from pprint import pformat
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from lms.djangoapps.verify_student.models import ManualVerification
|
||||
from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
This method attempts to manually verify users.
|
||||
Example usage:
|
||||
$ ./manage.py lms manual_verifications --email-ids-file <absolute path of file with email ids (one per line)>
|
||||
"""
|
||||
help = 'Manually verifies one or more users passed as an argument list.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--email-ids-file',
|
||||
action='store',
|
||||
dest='email_ids_file',
|
||||
default=None,
|
||||
help='Path of the file to read email id from.',
|
||||
type=str,
|
||||
required=True
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
email_ids_file = options['email_ids_file']
|
||||
|
||||
if email_ids_file:
|
||||
if not os.path.exists(email_ids_file):
|
||||
raise CommandError(u'Pass the correct absolute path to email ids file as --email-ids-file argument.')
|
||||
|
||||
total_emails, failed_emails = self._generate_manual_verification_from_file(email_ids_file)
|
||||
|
||||
if failed_emails:
|
||||
log.error(u'Completed manual verification. {} of {} failed.'.format(
|
||||
len(failed_emails),
|
||||
total_emails
|
||||
))
|
||||
log.error('Failed emails:{}'.format(pformat(failed_emails)))
|
||||
else:
|
||||
log.info('Successfully generated manual verification for {} emails.'.format(total_emails))
|
||||
|
||||
def _generate_manual_verification_from_file(self, email_ids_file):
|
||||
"""
|
||||
Generate manual verification for the emails provided in the email ids file.
|
||||
|
||||
Arguments:
|
||||
email_ids_file (str): path of the file containing email ids.
|
||||
|
||||
Returns:
|
||||
(total_emails, failed_emails): a tuple containing count of emails processed and a list containing
|
||||
emails whose verifications could not be processed.
|
||||
"""
|
||||
failed_emails = []
|
||||
|
||||
with open(email_ids_file, 'r') as file_handler:
|
||||
email_ids = file_handler.readlines()
|
||||
total_emails = len(email_ids)
|
||||
log.info(u'Creating manual verification for {} emails.'.format(total_emails))
|
||||
for email_id in email_ids:
|
||||
try:
|
||||
email_id = email_id.strip()
|
||||
user = User.objects.get(email=email_id)
|
||||
ManualVerification.objects.get_or_create(
|
||||
user=user,
|
||||
status='approved',
|
||||
created_at__gte=earliest_allowed_verification_date(),
|
||||
defaults={'name': user.profile.name},
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
failed_emails.append(email_id)
|
||||
err_msg = u'Tried to verify email {}, but user not found'
|
||||
log.error(err_msg.format(email_id))
|
||||
return total_emails, failed_emails
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Tests for django admin commands in the verify_student module
|
||||
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.core.management import call_command, CommandError
|
||||
from django.test import TestCase
|
||||
from lms.djangoapps.verify_student.models import ManualVerification
|
||||
from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date
|
||||
from student.tests.factories import UserFactory
|
||||
from testfixtures import LogCapture
|
||||
|
||||
LOGGER_NAME = 'lms.djangoapps.verify_student.management.commands.manual_verifications'
|
||||
|
||||
|
||||
class TestVerifyStudentCommand(TestCase):
|
||||
"""
|
||||
Tests for django admin commands in the verify_student module
|
||||
"""
|
||||
tmp_file_path = os.path.join(tempfile.gettempdir(), 'tmp-emails.txt')
|
||||
|
||||
def setUp(self):
|
||||
super(TestVerifyStudentCommand, self).setUp()
|
||||
self.user1 = UserFactory.create()
|
||||
self.user2 = UserFactory.create()
|
||||
self.user3 = UserFactory.create()
|
||||
self.invalid_email = unicode('unknown@unknown.com')
|
||||
|
||||
self.create_email_ids_file(
|
||||
self.tmp_file_path,
|
||||
[self.user1.email, self.user2.email, self.user3.email, self.invalid_email]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_email_ids_file(file_path, email_ids):
|
||||
"""
|
||||
Write the email_ids list to the temp file.
|
||||
"""
|
||||
with open(file_path, 'w') as temp_file:
|
||||
temp_file.write(str("\n".join(email_ids)))
|
||||
|
||||
def test_manual_verifications(self):
|
||||
"""
|
||||
Tests that the manual_verifications management command executes successfully
|
||||
"""
|
||||
self.assertEquals(ManualVerification.objects.filter(status='approved').count(), 0)
|
||||
|
||||
call_command('manual_verifications', '--email-ids-file', self.tmp_file_path)
|
||||
|
||||
self.assertEquals(ManualVerification.objects.filter(status='approved').count(), 3)
|
||||
|
||||
def test_manual_verifications_created_date(self):
|
||||
"""
|
||||
Tests that the manual_verifications management command does not create a new verification
|
||||
if a previous non-expired verification exists
|
||||
"""
|
||||
call_command('manual_verifications', '--email-ids-file', self.tmp_file_path)
|
||||
|
||||
verification1 = ManualVerification.objects.filter(
|
||||
user=self.user1,
|
||||
status='approved',
|
||||
created_at__gte=earliest_allowed_verification_date()
|
||||
)
|
||||
|
||||
call_command('manual_verifications', '--email-ids-file', self.tmp_file_path)
|
||||
|
||||
verification2 = ManualVerification.objects.filter(
|
||||
user=self.user1,
|
||||
status='approved',
|
||||
created_at__gte=earliest_allowed_verification_date()
|
||||
)
|
||||
|
||||
self.assertQuerysetEqual(verification1, [repr(r) for r in verification2])
|
||||
|
||||
def test_user_doesnot_exist_log(self):
|
||||
"""
|
||||
Tests that the manual_verifications management command logs an error when an invalid email is
|
||||
provided as input
|
||||
"""
|
||||
expected_log = (
|
||||
(LOGGER_NAME,
|
||||
'INFO',
|
||||
u'Creating manual verification for 4 emails.'
|
||||
),
|
||||
(LOGGER_NAME,
|
||||
'ERROR',
|
||||
u'Tried to verify email unknown@unknown.com, but user not found'
|
||||
),
|
||||
(LOGGER_NAME,
|
||||
'ERROR',
|
||||
u'Completed manual verification. 1 of 4 failed.'
|
||||
),
|
||||
(LOGGER_NAME,
|
||||
'ERROR',
|
||||
"Failed emails:['unknown@unknown.com']"
|
||||
)
|
||||
)
|
||||
with LogCapture(LOGGER_NAME, level=logging.INFO) as logger:
|
||||
call_command('manual_verifications', '--email-ids-file', self.tmp_file_path)
|
||||
|
||||
logger.check(
|
||||
*expected_log
|
||||
)
|
||||
|
||||
def test_invalid_file_path(self):
|
||||
"""
|
||||
Verify command raises the CommandError for invalid file path.
|
||||
"""
|
||||
with self.assertRaises(CommandError):
|
||||
call_command('manual_verifications', '--email-ids-file', u'invalid/email_id/file/path')
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-06-07 10:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import model_utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('verify_student', '0009_remove_id_verification_aggregate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ManualVerification',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', model_utils.fields.StatusField(choices=[(b'created', b'created'), (b'ready', b'ready'), (b'submitted', b'submitted'), (b'must_retry', b'must_retry'), (b'approved', b'approved'), (b'denied', b'denied')], default=b'created', max_length=100, no_check_for_status=True, verbose_name='status')),
|
||||
('status_changed', model_utils.fields.MonitorField(default=django.utils.timezone.now, monitor='status', verbose_name='status changed')),
|
||||
('name', models.CharField(blank=True, max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||
('reason', models.CharField(blank=True, help_text=b'Specifies the reason for manual verification of the user.', max_length=255)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -141,6 +141,36 @@ class IDVerificationAttempt(StatusModel):
|
||||
)
|
||||
|
||||
|
||||
class ManualVerification(IDVerificationAttempt):
|
||||
"""
|
||||
Each ManualVerification represents a user's verification that bypasses the need for
|
||||
any other verification.
|
||||
"""
|
||||
|
||||
reason = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=(
|
||||
'Specifies the reason for manual verification of the user.'
|
||||
)
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
app_label = 'verify_student'
|
||||
|
||||
def __unicode__(self):
|
||||
return 'ManualIDVerification for {name}, status: {status}'.format(
|
||||
name=self.name,
|
||||
status=self.status,
|
||||
)
|
||||
|
||||
def should_display_status_to_user(self):
|
||||
"""
|
||||
Whether or not the status should be displayed to the user.
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
class SSOVerification(IDVerificationAttempt):
|
||||
"""
|
||||
Each SSOVerification represents a Student's attempt to establish their identity
|
||||
|
||||
@@ -13,7 +13,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 SoftwareSecurePhotoVerification, SSOVerification
|
||||
from .models import SoftwareSecurePhotoVerification, SSOVerification, ManualVerification
|
||||
from .utils import earliest_allowed_verification_date, most_recent_verification
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -69,7 +69,8 @@ class IDVerificationService(object):
|
||||
}
|
||||
|
||||
return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
|
||||
SSOVerification.objects.filter(**filter_kwargs).exists())
|
||||
SSOVerification.objects.filter(**filter_kwargs).exists() or
|
||||
ManualVerification.objects.filter(**filter_kwargs).exists())
|
||||
|
||||
@classmethod
|
||||
def verifications_for_user(cls, user):
|
||||
@@ -78,7 +79,8 @@ class IDVerificationService(object):
|
||||
"""
|
||||
verifications = []
|
||||
for verification in chain(SoftwareSecurePhotoVerification.objects.filter(user=user),
|
||||
SSOVerification.objects.filter(user=user)):
|
||||
SSOVerification.objects.filter(user=user),
|
||||
ManualVerification.objects.filter(user=user)):
|
||||
verifications.append(verification)
|
||||
return verifications
|
||||
|
||||
@@ -95,7 +97,8 @@ class IDVerificationService(object):
|
||||
}
|
||||
return chain(
|
||||
SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).select_related('user'),
|
||||
SSOVerification.objects.filter(**filter_kwargs).select_related('user')
|
||||
SSOVerification.objects.filter(**filter_kwargs).select_related('user'),
|
||||
ManualVerification.objects.filter(**filter_kwargs).select_related('user')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -120,8 +123,14 @@ class IDVerificationService(object):
|
||||
|
||||
photo_id_verifications = SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs)
|
||||
sso_id_verifications = SSOVerification.objects.filter(**filter_kwargs)
|
||||
manual_id_verifications = ManualVerification.objects.filter(**filter_kwargs)
|
||||
|
||||
attempt = most_recent_verification(photo_id_verifications, sso_id_verifications, 'updated_at')
|
||||
attempt = most_recent_verification(
|
||||
photo_id_verifications,
|
||||
sso_id_verifications,
|
||||
manual_id_verifications,
|
||||
'updated_at'
|
||||
)
|
||||
return attempt and attempt.expiration_datetime
|
||||
|
||||
@classmethod
|
||||
@@ -139,7 +148,8 @@ class IDVerificationService(object):
|
||||
}
|
||||
|
||||
return (SoftwareSecurePhotoVerification.objects.filter(**filter_kwargs).exists() or
|
||||
SSOVerification.objects.filter(**filter_kwargs).exists())
|
||||
SSOVerification.objects.filter(**filter_kwargs).exists() or
|
||||
ManualVerification.objects.filter(**filter_kwargs).exists())
|
||||
|
||||
@classmethod
|
||||
def user_status(cls, user):
|
||||
@@ -166,8 +176,14 @@ class IDVerificationService(object):
|
||||
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')
|
||||
manual_id_verifications = ManualVerification.objects.filter(user=user).order_by('-updated_at')
|
||||
|
||||
attempt = most_recent_verification(photo_id_verifications, sso_id_verifications, 'updated_at')
|
||||
attempt = most_recent_verification(
|
||||
photo_id_verifications,
|
||||
sso_id_verifications,
|
||||
manual_id_verifications,
|
||||
'updated_at'
|
||||
)
|
||||
except IndexError:
|
||||
# The user has no verification attempts, return the default set of data.
|
||||
return user_status
|
||||
|
||||
@@ -23,6 +23,7 @@ from common.test.utils import MockS3Mixin
|
||||
from lms.djangoapps.verify_student.models import (
|
||||
SoftwareSecurePhotoVerification,
|
||||
SSOVerification,
|
||||
ManualVerification,
|
||||
VerificationDeadline,
|
||||
VerificationException
|
||||
)
|
||||
@@ -387,6 +388,16 @@ class SSOVerificationTest(TestVerification):
|
||||
self.verification_active_at_datetime(attempt)
|
||||
|
||||
|
||||
class ManualVerificationTest(TestVerification):
|
||||
"""
|
||||
Tests for the ManualVerification model
|
||||
"""
|
||||
def test_active_at_datetime(self):
|
||||
user = UserFactory.create()
|
||||
verification = ManualVerification.objects.create(user=user)
|
||||
self.verification_active_at_datetime(verification)
|
||||
|
||||
|
||||
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, SSOVerification, ManualVerification
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -78,12 +78,12 @@ 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.assertDictEqual(status, {'status': 'none', 'error': '', 'should_display': True})
|
||||
|
||||
# test for when photo verification has been created
|
||||
SoftwareSecurePhotoVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': True})
|
||||
self.assertDictEqual(status, {'status': 'approved', 'error': '', 'should_display': True})
|
||||
|
||||
# create another photo verification for the same user, make sure the denial
|
||||
# is handled properly
|
||||
@@ -91,18 +91,23 @@ class TestIDVerificationService(MockS3Mixin, ModuleStoreTestCase):
|
||||
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.assertDictEqual(status, {'status': 'must_reverify', 'error': ['id_image_missing'], 'should_display': True})
|
||||
|
||||
# test for when sso verification has been created
|
||||
SSOVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertEquals(status, {'status': 'approved', 'error': '', 'should_display': False})
|
||||
self.assertDictEqual(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.assertDictEqual(status, {'status': 'must_reverify', 'error': '', 'should_display': False})
|
||||
|
||||
# test for when manual verification has been created
|
||||
ManualVerification.objects.create(user=user, status='approved')
|
||||
status = IDVerificationService.user_status(user)
|
||||
self.assertDictEqual(status, {'status': 'approved', 'error': '', 'should_display': False})
|
||||
|
||||
@ddt.unpack
|
||||
@ddt.data(
|
||||
|
||||
@@ -11,7 +11,7 @@ import pytz
|
||||
from mock import patch
|
||||
from pytest import mark
|
||||
from django.conf import settings
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification, ManualVerification
|
||||
from lms.djangoapps.verify_student.utils import verification_for_datetime, most_recent_verification
|
||||
from student.tests.factories import UserFactory
|
||||
|
||||
@@ -88,38 +88,49 @@ class TestVerifyStudentUtils(unittest.TestCase):
|
||||
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'),
|
||||
(False, False, False, None, None),
|
||||
(True, False, False, None, 'photo'),
|
||||
(False, True, False, None, 'sso'),
|
||||
(False, False, True, None, 'manual'),
|
||||
(True, True, True, 'photo', 'sso'),
|
||||
(True, True, True, 'sso', 'photo'),
|
||||
(True, True, True, 'manual', 'photo')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_most_recent_verification(
|
||||
self,
|
||||
create_photo_verification,
|
||||
create_sso_verification,
|
||||
create_manual_verification,
|
||||
first_verification,
|
||||
expected_verification):
|
||||
|
||||
user = UserFactory.create()
|
||||
photo_verification = None
|
||||
sso_verification = None
|
||||
manual_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)
|
||||
if create_manual_verification:
|
||||
manual_verification = ManualVerification.objects.create(user=user)
|
||||
elif first_verification == 'photo':
|
||||
photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
|
||||
sso_verification = SSOVerification.objects.create(user=user)
|
||||
else:
|
||||
elif first_verification == 'sso':
|
||||
sso_verification = SSOVerification.objects.create(user=user)
|
||||
photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
|
||||
else:
|
||||
manual_verification = ManualVerification.objects.create(user=user)
|
||||
photo_verification = SoftwareSecurePhotoVerification.objects.create(user=user)
|
||||
|
||||
most_recent = most_recent_verification(
|
||||
SoftwareSecurePhotoVerification.objects.all(),
|
||||
SSOVerification.objects.all(),
|
||||
ManualVerification.objects.all(),
|
||||
'created_at'
|
||||
)
|
||||
|
||||
@@ -127,5 +138,7 @@ class TestVerifyStudentUtils(unittest.TestCase):
|
||||
self.assertEqual(most_recent, None)
|
||||
elif expected_verification == 'photo':
|
||||
self.assertEqual(most_recent, photo_verification)
|
||||
else:
|
||||
elif expected_verification == 'sso':
|
||||
self.assertEqual(most_recent, sso_verification)
|
||||
else:
|
||||
self.assertEqual(most_recent, manual_verification)
|
||||
|
||||
@@ -93,28 +93,32 @@ def send_verification_status_email(context):
|
||||
))
|
||||
|
||||
|
||||
def most_recent_verification(photo_id_verifications, sso_id_verifications, most_recent_key):
|
||||
def most_recent_verification(photo_id_verifications, sso_id_verifications, manual_id_verifications, most_recent_key):
|
||||
"""
|
||||
Return the most recent verification given querysets for both photo and sso verifications.
|
||||
Return the most recent verification given querysets for photo, sso and manual verifications.
|
||||
|
||||
This function creates a map of the latest verification of all types and then returns the earliest
|
||||
verification using the max of the map values.
|
||||
|
||||
Arguments:
|
||||
photo_id_verifications: Queryset containing photo verifications
|
||||
sso_id_verifications: Queryset containing sso verifications
|
||||
most_recent_key: Either 'updated_at' or 'created_at'
|
||||
photo_id_verifications: Queryset containing photo verifications
|
||||
sso_id_verifications: Queryset containing sso verifications
|
||||
manual_id_verifications: Queryset containing manual 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()
|
||||
manual_id_verification = manual_id_verifications and manual_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
|
||||
verifications = [photo_id_verification, sso_id_verification, manual_id_verification]
|
||||
|
||||
verifications_map = {
|
||||
verification: getattr(verification, most_recent_key)
|
||||
for verification in verifications
|
||||
if getattr(verification, most_recent_key, False)
|
||||
}
|
||||
|
||||
return max(verifications_map, key=lambda k: verifications_map[k]) if verifications_map else None
|
||||
|
||||
@@ -162,7 +162,7 @@ class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin, CredentialsAp
|
||||
call_command('backpopulate_program_credentials', commit=True)
|
||||
|
||||
# The task should be called for both users since professional and no-id-professional are equivalent.
|
||||
mock_task.assert_has_calls([mock.call(self.alice.username), mock.call(self.bob.username)])
|
||||
mock_task.assert_has_calls([mock.call(self.alice.username), mock.call(self.bob.username)], any_order=True)
|
||||
|
||||
@ddt.data(SEPARATE_PROGRAMS, SEPARATE_COURSES, SAME_COURSE)
|
||||
def test_handle_flatten(self, hierarchy_type, mock_task, mock_get_programs):
|
||||
|
||||
@@ -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, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification, ManualVerification
|
||||
|
||||
from .models import UserPreference
|
||||
|
||||
@@ -119,3 +119,10 @@ class SSOVerificationSerializer(IDVerificationSerializer):
|
||||
class Meta(object):
|
||||
fields = ('status', 'expiration_datetime', 'is_verified')
|
||||
model = SSOVerification
|
||||
|
||||
|
||||
class ManualVerificationSerializer(IDVerificationSerializer):
|
||||
|
||||
class Meta(object):
|
||||
fields = ('status', 'expiration_datetime', 'is_verified')
|
||||
model = ManualVerification
|
||||
|
||||
@@ -5,10 +5,10 @@ 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, SSOVerification
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, SSOVerification, ManualVerification
|
||||
from lms.djangoapps.verify_student.utils import most_recent_verification
|
||||
from openedx.core.djangoapps.user_api.serializers import (
|
||||
SoftwareSecurePhotoVerificationSerializer, SSOVerificationSerializer,
|
||||
SoftwareSecurePhotoVerificationSerializer, SSOVerificationSerializer, ManualVerificationSerializer,
|
||||
)
|
||||
from openedx.core.lib.api.permissions import IsStaffOrOwner
|
||||
|
||||
@@ -26,17 +26,25 @@ class IDVerificationStatusView(RetrieveAPIView):
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
if isinstance(instance, SoftwareSecurePhotoVerification):
|
||||
return SoftwareSecurePhotoVerificationSerializer(*args, **kwargs)
|
||||
else:
|
||||
elif isinstance(instance, SSOVerification):
|
||||
return SSOVerificationSerializer(*args, **kwargs)
|
||||
else:
|
||||
return ManualVerificationSerializer(*args, **kwargs)
|
||||
|
||||
def get_object(self):
|
||||
username = self.kwargs['username']
|
||||
photo_verifications = SoftwareSecurePhotoVerification.objects.filter(
|
||||
user__username=username).order_by('-updated_at')
|
||||
sso_verifications = SSOVerification.objects.filter(user__username=username).order_by('-updated_at')
|
||||
manual_verifications = ManualVerification.objects.filter(user__username=username).order_by('-updated_at')
|
||||
|
||||
if photo_verifications or sso_verifications:
|
||||
verification = most_recent_verification(photo_verifications, sso_verifications, 'updated_at')
|
||||
if photo_verifications or sso_verifications or manual_verifications:
|
||||
verification = most_recent_verification(
|
||||
photo_verifications,
|
||||
sso_verifications,
|
||||
manual_verifications,
|
||||
'updated_at'
|
||||
)
|
||||
self.check_object_permissions(self.request, verification)
|
||||
return verification
|
||||
|
||||
|
||||
Reference in New Issue
Block a user