populate expiry date and add mechanism for new verification

LEARNER-7136 populate expiry date and add mechanism for new verification

Method approve() is overridden to accommodate new users that will have their verification approved in the future. The approve() method is called from software secure results callback every time a Software Secure Photo Verification is approved.The method overridden now updates expiry_date and then calls super to perform task it was performing before this change.The expiry_date set is the same as the one sent in verification approval email.

In results_callback for sspv the expiry_date and expiry_email_date for the most recent previous verification if exists are also updated to NULL.
This commit is contained in:
Zainab Amir
2019-03-07 11:29:09 +05:00
committed by GitHub
5 changed files with 305 additions and 1 deletions

View File

@@ -0,0 +1,104 @@
"""
Django admin command to populate expiry_date for approved verifications in SoftwareSecurePhotoVerification
"""
import logging
import time
from datetime import timedelta
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import F
from util.query import use_read_replica_if_available
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
This command sets the expiry_date for users for which the verification is approved
The task is performed in batches with maximum number of rows to process given in argument `batch_size`
and a sleep time between each batch given by `sleep_time`
Default values:
`batch_size` = 1000 rows
`sleep_time` = 10 seconds
Example usage:
$ ./manage.py lms populate_expiry_date --batch_size=1000 --sleep_time=5
OR
$ ./manage.py lms populate_expiry_date
"""
help = 'Populate expiry_date for approved verifications'
def add_arguments(self, parser):
parser.add_argument(
'--batch_size',
action='store',
dest='batch_size',
type=int,
default=1000,
help='Maximum number of database rows to process. '
'This helps avoid locking the database while updating large amount of data.')
parser.add_argument(
'--sleep_time',
action='store',
dest='sleep_time',
type=int,
default=10,
help='Sleep time in seconds between update of batches')
def handle(self, *args, **options):
"""
Handler for the command
It filters approved Software Secure Photo Verification and then for each distinct user it finds the most
recent approved verification and set its expiry_date
"""
batch_size = options['batch_size']
sleep_time = options['sleep_time']
query = SoftwareSecurePhotoVerification.objects.filter(status='approved').order_by()
sspv = use_read_replica_if_available(query)
if not sspv.count():
logger.info("No approved entries found in SoftwareSecurePhotoVerification")
return
distinct_user_ids = set()
update_verification_ids = []
update_verification_count = 0
for verification in sspv:
if verification.user_id not in distinct_user_ids:
distinct_user_ids.add(verification.user_id)
recent_verification = self.find_recent_verification(sspv, verification.user_id)
if not recent_verification.expiry_date:
update_verification_ids.append(recent_verification.pk)
update_verification_count += 1
if update_verification_count == batch_size:
self.bulk_update(update_verification_ids)
update_verification_count = 0
update_verification_ids = []
time.sleep(sleep_time)
if update_verification_ids:
self.bulk_update(update_verification_ids)
def bulk_update(self, verification_ids):
"""
It updates the expiry_date for all the verification whose ids lie in verification_ids
"""
recent_verification_qs = SoftwareSecurePhotoVerification.objects.filter(pk__in=verification_ids)
recent_verification_qs.update(expiry_date=F('updated_at') + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]))
def find_recent_verification(self, model, user_id):
"""
Returns the most recent approved verification for a user
"""
return model.filter(user_id=user_id).latest('updated_at')

View File

@@ -0,0 +1,124 @@
"""
Tests for django admin command `populate_expiry_date` in the verify_student module
"""
from datetime import timedelta
import boto
from django.conf import settings
from django.core.management import call_command
from django.test import TestCase
from mock import patch
from student.tests.factories import UserFactory
from testfixtures import LogCapture
from common.test.utils import MockS3Mixin
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from lms.djangoapps.verify_student.tests.test_models import (
FAKE_SETTINGS,
mock_software_secure_post
)
LOGGER_NAME = 'lms.djangoapps.verify_student.management.commands.populate_expiry_date'
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
@patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post)
class TestPopulateExpiryDate(MockS3Mixin, TestCase):
""" Tests for django admin command `populate_expiry_date` in the verify_student module """
def setUp(self):
""" Initial set up for tests """
super(TestPopulateExpiryDate, self).setUp()
connection = boto.connect_s3()
connection.create_bucket(FAKE_SETTINGS['SOFTWARE_SECURE']['S3_BUCKET'])
def create_and_submit(self, user):
""" Helper method that lets us create new SoftwareSecurePhotoVerifications """
attempt = SoftwareSecurePhotoVerification(user=user)
attempt.upload_face_image("Fake Data")
attempt.upload_photo_id_image("More Fake Data")
attempt.mark_ready()
attempt.submit()
return attempt
def test_expiry_date_already_present(self):
"""
Test that the expiry_date for most recent approved verification is updated only when the
expiry_date is not already present
"""
user = UserFactory.create()
verification = self.create_and_submit(user)
verification.status = 'approved'
verification.expiry_date = verification.updated_at + timedelta(days=10)
verification.save()
expiry_date = verification.expiry_date
call_command('populate_expiry_date')
# Check that the expiry_date for approved verification is not changed when it is already present
verification_expiry_date = SoftwareSecurePhotoVerification.objects.get(pk=verification.pk).expiry_date
self.assertEqual(verification_expiry_date, expiry_date)
def test_recent_approved_verification(self):
"""
Test that the expiry_date for most recent approved verification is updated
A user can have multiple approved Software Secure Photo Verification over the year
Only the most recent is considered for course verification
"""
user = UserFactory.create()
outdated_verification = self.create_and_submit(user)
outdated_verification.status = 'approved'
outdated_verification.save()
recent_verification = self.create_and_submit(user)
recent_verification.status = 'approved'
recent_verification.save()
call_command('populate_expiry_date')
# Check that expiry_date for only one verification is set
assert len(SoftwareSecurePhotoVerification.objects.filter(expiry_date__isnull=False)) == 1
# Check that the expiry_date date set for verification is not for the outdated approved verification
expiry_date = SoftwareSecurePhotoVerification.objects.get(pk=outdated_verification.pk).expiry_date
self.assertIsNone(expiry_date)
def test_approved_verification_expiry_date(self):
"""
Tests that the command correctly updates expiry_date
Criteria :
Verification for which status is approved and expiry_date is null
"""
# Create verification with status : submitted
user = UserFactory.create()
self.create_and_submit(user)
# Create verification with status : approved
approved_verification = self.create_and_submit(user)
approved_verification.status = 'approved'
approved_verification.save()
expected_date = approved_verification.updated_at + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
call_command('populate_expiry_date')
# Check to make sure we have one verification with expiry_date set and one with null
assert len(SoftwareSecurePhotoVerification.objects.filter(expiry_date__isnull=True)) == 1
assert len(SoftwareSecurePhotoVerification.objects.filter(expiry_date__isnull=False)) == 1
# Confirm that expiry_date set for approved verification is correct
approved_verification = SoftwareSecurePhotoVerification.objects.get(pk=approved_verification.pk)
self.assertEqual(approved_verification.expiry_date, expected_date)
def test_no_approved_verification_found(self):
"""
Test that if no approved verifications are found the management command terminates gracefully
"""
with LogCapture(LOGGER_NAME) as logger:
call_command('populate_expiry_date')
logger.check(
(LOGGER_NAME, 'INFO', "No approved entries found in SoftwareSecurePhotoVerification")
)

View File

@@ -561,6 +561,23 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
expiry_date = models.DateTimeField(null=True, blank=True, db_index=True)
expiry_email_date = models.DateTimeField(null=True, blank=True, db_index=True)
@status_before_must_be("must_retry", "submitted", "approved", "denied")
def approve(self, user_id=None, service=""):
"""
Approve the verification attempt for user
Valid attempt statuses when calling this method:
`submitted`, `approved`, `denied`
After method completes:
status is set to `approved`
expiry_date is set to one year from now
"""
self.expiry_date = datetime.now(pytz.UTC) + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
)
super(SoftwareSecurePhotoVerification, self).approve(user_id, service)
@classmethod
def get_initial_verification(cls, user, earliest_allowed_date=None):
"""Get initial verification for a user with the 'photo_id_key'.

View File

@@ -1764,6 +1764,17 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
"""
Test for verification passed.
"""
expiry_date = datetime.now(pytz.UTC) + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
)
verification = SoftwareSecurePhotoVerification.objects.create(user=self.user)
verification.mark_ready()
verification.submit()
verification.approve()
verification.expiry_date = datetime.now(pytz.UTC)
verification.expiry_email_date = datetime.now(pytz.UTC)
verification.save()
data = {
"EdX-ID": self.receipt_id,
"Result": "PASS",
@@ -1778,7 +1789,45 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase):
HTTP_DATE='testdate'
)
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
old_verification = SoftwareSecurePhotoVerification.objects.get(pk=verification.pk)
self.assertEqual(attempt.status, u'approved')
self.assertEqual(attempt.expiry_date.date(), expiry_date.date())
self.assertIsNone(old_verification.expiry_date)
self.assertIsNone(old_verification.expiry_email_date)
self.assertEquals(response.content, 'OK!')
self.assertEqual(len(mail.outbox), 1)
@patch(
'lms.djangoapps.verify_student.ssencrypt.has_valid_signature',
mock.Mock(side_effect=mocked_has_valid_signature)
)
@patch('lms.djangoapps.verify_student.views.log.error')
@patch('lms.djangoapps.verify_student.utils.SailthruClient.send')
def test_first_time_verification(self, mock_sailthru_send, mock_log_error): # pylint: disable=unused-argument
"""
Test for verification passed if the learner does not have any previous verification
"""
expiry_date = datetime.now(pytz.UTC) + timedelta(
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]
)
data = {
"EdX-ID": self.receipt_id,
"Result": "PASS",
"Reason": "",
"MessageType": "You have been verified."
}
json_data = json.dumps(data)
response = self.client.post(
reverse('verify_student_results_callback'), data=json_data,
content_type='application/json',
HTTP_AUTHORIZATION='test BBBBBBBBBBBBBBBBBBBB:testing',
HTTP_DATE='testdate'
)
attempt = SoftwareSecurePhotoVerification.objects.get(receipt_id=self.receipt_id)
self.assertEqual(attempt.status, u'approved')
self.assertEqual(attempt.expiry_date.date(), expiry_date.date())
self.assertEquals(response.content, 'OK!')
self.assertEqual(len(mail.outbox), 1)

View File

@@ -1158,7 +1158,17 @@ def results_callback(request):
'platform_name': settings.PLATFORM_NAME,
}
if result == "PASS":
log.debug(u"Approving verification for %s", receipt_id)
# If this verification is not an outdated version then make expiry date of previous approved verification NULL
# Setting expiry date to NULL is important so that it does not get filtered in the management command
# that sends email when verification expires : verify_student/send_verification_expiry_email
if attempt.status != 'approved':
verification = SoftwareSecurePhotoVerification.objects.filter(status='approved', user_id=attempt.user_id)
if verification:
log.info(u'Making expiry date of previous approved verification NULL for {}'.format(attempt.user_id))
previous_verification = verification.latest('updated_at')
SoftwareSecurePhotoVerification.objects.filter(pk=previous_verification.pk
).update(expiry_date=None, expiry_email_date=None)
log.debug(u'Approving verification for {}'.format(receipt_id))
attempt.approve()
status = u"approved"
expiry_date = datetime.date.today() + datetime.timedelta(