diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index bd489d7e6e6bf942d2551c12c45fb6bda730d288..e761465f361a3c7f2d220a9dd9a764a79e93d26d 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -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 = { diff --git a/lms/djangoapps/verify_student/admin.py b/lms/djangoapps/verify_student/admin.py index 8a2e8315ddc719809b3d3520eafbcbdfa2114df9..3db76cda4c4e98ef03a18eef37aa63297de8e3d4 100644 --- a/lms/djangoapps/verify_student/admin.py +++ b/lms/djangoapps/verify_student/admin.py @@ -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',) diff --git a/lms/djangoapps/verify_student/management/commands/manual_verifications.py b/lms/djangoapps/verify_student/management/commands/manual_verifications.py new file mode 100644 index 0000000000000000000000000000000000000000..37421332fd7e63a28aefd4851c0db65bd4ffd868 --- /dev/null +++ b/lms/djangoapps/verify_student/management/commands/manual_verifications.py @@ -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 diff --git a/lms/djangoapps/verify_student/management/commands/tests/test_manual_verify_student.py b/lms/djangoapps/verify_student/management/commands/tests/test_manual_verify_student.py new file mode 100644 index 0000000000000000000000000000000000000000..134386dbfb33d8e16ab42b750cb083af6702039c --- /dev/null +++ b/lms/djangoapps/verify_student/management/commands/tests/test_manual_verify_student.py @@ -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') diff --git a/lms/djangoapps/verify_student/migrations/0010_manualverification.py b/lms/djangoapps/verify_student/migrations/0010_manualverification.py new file mode 100644 index 0000000000000000000000000000000000000000..55bcdbff4b99d3b410fa8b673fefc79272610641 --- /dev/null +++ b/lms/djangoapps/verify_student/migrations/0010_manualverification.py @@ -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)), + ], + ), + ] diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 9fc15d1bab505327725d12273c66a52194e7cfde..d21617493b04e616d6dac217f313915b444d461c 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -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 diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py index 8e8cbc026a8ebfa5345065a71e791917660401d0..7fa51c474e1d7c1ce2eeeccd4e76d24bdfab6a71 100644 --- a/lms/djangoapps/verify_student/services.py +++ b/lms/djangoapps/verify_student/services.py @@ -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') - - attempt = most_recent_verification(photo_id_verifications, sso_id_verifications, 'updated_at') + manual_id_verifications = ManualVerification.objects.filter(user=user).order_by('-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 diff --git a/lms/djangoapps/verify_student/tests/test_models.py b/lms/djangoapps/verify_student/tests/test_models.py index 1dbbb126ea7bff0917928e1c8339dab874cd38bf..c55a097206255446cc7e105f0acda10cc4f41e53 100644 --- a/lms/djangoapps/verify_student/tests/test_models.py +++ b/lms/djangoapps/verify_student/tests/test_models.py @@ -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. diff --git a/lms/djangoapps/verify_student/tests/test_services.py b/lms/djangoapps/verify_student/tests/test_services.py index 68410e9d503506e5f76e94befe9791614dde0928..43999288bf32d07a2cf3053757c22bba68624227 100644 --- a/lms/djangoapps/verify_student/tests/test_services.py +++ b/lms/djangoapps/verify_student/tests/test_services.py @@ -16,7 +16,7 @@ from nose.tools import ( ) from common.test.utils import MockS3Mixin -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, 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( diff --git a/lms/djangoapps/verify_student/tests/test_utils.py b/lms/djangoapps/verify_student/tests/test_utils.py index 75407968d4ed859b37d5430471fe2b5c27ef68ef..49806b681cc10a0a70c90b36889765e266381e03 100644 --- a/lms/djangoapps/verify_student/tests/test_utils.py +++ b/lms/djangoapps/verify_student/tests/test_utils.py @@ -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) diff --git a/lms/djangoapps/verify_student/utils.py b/lms/djangoapps/verify_student/utils.py index 3193d0055d50f0e58a083971590c3c9dd688f003..5fb4e11c9cb4b4d077841e563df548548993b0f8 100644 --- a/lms/djangoapps/verify_student/utils.py +++ b/lms/djangoapps/verify_student/utils.py @@ -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 diff --git a/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py b/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py index fb48035b467fed68c6cd266533dcc44948cc7210..6b93d85e495cbf857f255c28c7d179b333cdb51e 100644 --- a/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py +++ b/openedx/core/djangoapps/programs/tests/test_backpopulate_program_credentials.py @@ -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): diff --git a/openedx/core/djangoapps/user_api/serializers.py b/openedx/core/djangoapps/user_api/serializers.py index 7b24ab35b50929dd36aad79bbd33941fa567c42c..ae515de0ae23843faa73eb8964738e1cb42cb90b 100644 --- a/openedx/core/djangoapps/user_api/serializers.py +++ b/openedx/core/djangoapps/user_api/serializers.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from django.utils.timezone import now from rest_framework import serializers -from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, 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 diff --git a/openedx/core/djangoapps/user_api/verification_api/views.py b/openedx/core/djangoapps/user_api/verification_api/views.py index eae82f74e11d6caea66359e7e4a27497dcdc7a80..b64714ba46ce30d9910290c039fbcc1d739f508c 100644 --- a/openedx/core/djangoapps/user_api/verification_api/views.py +++ b/openedx/core/djangoapps/user_api/verification_api/views.py @@ -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