Merge pull request #27614 from edx/bseverino/remove-expiry-date
[MST-785] Remove references to expiry_date in IDV
This commit is contained in:
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
(DEPRECATED) 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 common.djangoapps.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')
|
||||
@@ -104,12 +104,6 @@ class Command(BaseCommand):
|
||||
Q(expiration_date__isnull=False) & (
|
||||
Q(expiration_date__gte=start_date, expiration_date__lt=end_date) |
|
||||
Q(expiry_email_date__lte=date_resend_days_ago)
|
||||
) |
|
||||
# Account for old entries still using `expiry_date` rather than`expiration_date`
|
||||
# (this will be deprecated)
|
||||
Q(expiry_date__isnull=False) & (
|
||||
Q(expiry_date__gte=start_date, expiry_date__lt=end_date) |
|
||||
Q(expiry_email_date__lte=date_resend_days_ago)
|
||||
)
|
||||
)
|
||||
).order_by()
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
"""
|
||||
Tests for django admin command `populate_expiry_date` in the verify_student module
|
||||
"""
|
||||
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.test.utils import MockS3BotoMixin
|
||||
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(MockS3BotoMixin, TestCase):
|
||||
""" Tests for django admin command `populate_expiry_date` in the verify_student module """
|
||||
|
||||
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
|
||||
|
||||
assert 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
|
||||
assert expiry_date is None
|
||||
|
||||
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)
|
||||
assert 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")
|
||||
)
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Tests for django admin command `update_expiration_date` in the verify_student module
|
||||
"""
|
||||
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from testfixtures import LogCapture
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.test.utils import MockS3BotoMixin
|
||||
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.update_expiration_date'
|
||||
|
||||
|
||||
@patch.dict(settings.VERIFY_STUDENT, FAKE_SETTINGS)
|
||||
@patch('lms.djangoapps.verify_student.models.requests.post', new=mock_software_secure_post)
|
||||
class TestPopulateExpiryationDate(MockS3BotoMixin, TestCase):
|
||||
""" Tests for django admin command `update_expiration_date` in the verify_student module """
|
||||
|
||||
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()
|
||||
attempt.expiry_date = now() + timedelta(days=FAKE_SETTINGS["DAYS_GOOD_FOR"])
|
||||
return attempt
|
||||
|
||||
def test_expiration_date_not_changed(self):
|
||||
"""
|
||||
Test that the `expiration_date` is updated only when its value is over 365 days higher
|
||||
than `updated_at`
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
verification = self.create_and_submit(user)
|
||||
expected_date = verification.expiration_date
|
||||
call_command('update_expiration_date')
|
||||
# Check that the `expiration_date` is not changed
|
||||
expiration_date = SoftwareSecurePhotoVerification.objects.get(pk=verification.pk).expiration_date
|
||||
assert expiration_date == expected_date
|
||||
|
||||
def test_expiration_date_updated(self):
|
||||
"""
|
||||
Test that `expiration_date` is properly updated when its value is over 365 days higher
|
||||
than `updated_at`
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
verification = self.create_and_submit(user)
|
||||
verification.status = 'approved'
|
||||
verification.expiration_date = now() + timedelta(days=FAKE_SETTINGS["DAYS_GOOD_FOR"] + 1)
|
||||
verification.expiry_date = now() + timedelta(days=FAKE_SETTINGS["DAYS_GOOD_FOR"])
|
||||
verification.save()
|
||||
expected_date = verification.created_at + timedelta(
|
||||
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
||||
call_command('update_expiration_date')
|
||||
updated_verification = SoftwareSecurePhotoVerification.objects.get(pk=verification.pk)
|
||||
# Check that the `expiration_date` is updated
|
||||
assert updated_verification.expiration_date == expected_date
|
||||
# Check that the `expiry_date` is set to NULL
|
||||
assert updated_verification.expiry_date is None
|
||||
|
||||
def test_expiration_date_updated_multiple_records(self):
|
||||
"""
|
||||
Test that `expiration_date` is properly updated with multiple records fits the selection
|
||||
criteria
|
||||
"""
|
||||
users = UserFactory.create_batch(10)
|
||||
for user in users:
|
||||
verification = self.create_and_submit(user)
|
||||
verification.status = 'approved'
|
||||
verification.expiry_date = now() + timedelta(days=3)
|
||||
verification.expiration_date = '2021-11-12T16:33:15.691Z'
|
||||
verification.save()
|
||||
|
||||
call_command('update_expiration_date', batch_size=3, sleep_time=1)
|
||||
updated_verifications = SoftwareSecurePhotoVerification.objects.filter(user__in=users)
|
||||
for updated_verification in updated_verifications:
|
||||
expected_date = updated_verification.created_at + timedelta(
|
||||
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
||||
# Check that the `expiration_date` is updated
|
||||
assert updated_verification.expiration_date == expected_date
|
||||
# Check that the `expiry_date` is set to NULL
|
||||
assert updated_verification.expiry_date is None
|
||||
|
||||
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('update_expiration_date')
|
||||
logger.check(
|
||||
(LOGGER_NAME, 'INFO', "No approved entries found in SoftwareSecurePhotoVerification")
|
||||
)
|
||||
@@ -1,96 +0,0 @@
|
||||
"""
|
||||
Django admin command to update expiration_date for approved verifications in SoftwareSecurePhotoVerification
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta # lint-amnesty, pylint: disable=unused-import
|
||||
|
||||
from django.conf import settings # lint-amnesty, pylint: disable=unused-import
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import F
|
||||
|
||||
from common.djangoapps.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 updates the `expiration_date` for old entries still dependent on `expiry_date`
|
||||
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 update_expiration_date --batch_size=1000 --sleep_time=5
|
||||
OR
|
||||
$ ./manage.py lms update_expiration_date
|
||||
"""
|
||||
help = 'Update expiration_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 Verifications and then sets the correct expiration_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
|
||||
|
||||
update_verification_ids = []
|
||||
update_verification_count = 0
|
||||
|
||||
for verification in sspv:
|
||||
# The expiration date should not be higher than 365 days
|
||||
# past the `updated_at` field, so only update those entries
|
||||
if verification.expiration_date > (
|
||||
verification.updated_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"])
|
||||
):
|
||||
update_verification_ids.append(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 expiration_date and sets the expiry_date to NULL for all the
|
||||
verifications whose ids lie in verification_ids
|
||||
"""
|
||||
verification_qs = SoftwareSecurePhotoVerification.objects.filter(pk__in=verification_ids)
|
||||
verification_qs.update(expiration_date=F('created_at') + timedelta(
|
||||
days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]))
|
||||
verification_qs.update(expiry_date=None)
|
||||
@@ -647,13 +647,6 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
|
||||
# to notify for expired verification is already sent.
|
||||
expiry_email_date = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||
|
||||
@property
|
||||
def expiration_datetime(self):
|
||||
"""Use expiry_date for older entries if it still exists."""
|
||||
if self.expiry_date:
|
||||
return self.expiry_date
|
||||
return super().expiration_datetime
|
||||
|
||||
@classmethod
|
||||
def get_initial_verification(cls, user, earliest_allowed_date=None):
|
||||
"""Get initial verification for a user with the 'photo_id_key'.
|
||||
|
||||
@@ -390,19 +390,6 @@ class TestPhotoVerification(TestVerificationBase, MockS3BotoMixin, ModuleStoreTe
|
||||
|
||||
assert verification.expiration_datetime == (verification.created_at + timedelta(days=FAKE_SETTINGS['DAYS_GOOD_FOR']))
|
||||
|
||||
def test_deprecated_expiry_date(self):
|
||||
"""
|
||||
Test `expiration_datetime` returns `expiry_date` if it is not null.
|
||||
"""
|
||||
user = UserFactory.create()
|
||||
with freeze_time(now()):
|
||||
verification = SoftwareSecurePhotoVerification(user=user)
|
||||
# First, assert that expiration_date is set correctly
|
||||
assert verification.expiration_datetime == (now() + timedelta(days=FAKE_SETTINGS['DAYS_GOOD_FOR']))
|
||||
verification.expiry_date = now() + timedelta(days=10)
|
||||
# Then, assert that expiration_datetime favors expiry_date's value if set
|
||||
assert verification.expiration_datetime == (now() + timedelta(days=10))
|
||||
|
||||
def test_get_verification_from_receipt(self):
|
||||
result = SoftwareSecurePhotoVerification.get_verification_from_receipt('')
|
||||
assert result is None
|
||||
|
||||
Reference in New Issue
Block a user