From f94a7f7f68b3127984b9ac3088ae1167a1521dd2 Mon Sep 17 00:00:00 2001 From: michaelroytman Date: Wed, 29 May 2024 16:35:33 -0400 Subject: [PATCH] feat: send IDV approval email in approve_id_verifications management command This commit modifies the approve_id_verifications management command to send an IDV approval email to learners. This ensures that learners are informed of approvals to their IDV attempts when performed using the management command. This more closely mirrors the way IDV approvals work when using an IDV vendor. --- lms/djangoapps/verify_student/api.py | 35 +++++++++++++++ .../commands/approve_id_verifications.py | 6 ++- .../tests/test_approve_id_verifications.py | 30 +++++++++++++ .../verify_student/tests/test_api.py | 43 +++++++++++++++++++ lms/djangoapps/verify_student/views.py | 22 ++-------- 5 files changed, 115 insertions(+), 21 deletions(-) create mode 100644 lms/djangoapps/verify_student/tests/test_api.py diff --git a/lms/djangoapps/verify_student/api.py b/lms/djangoapps/verify_student/api.py index e69de29bb2d1..c974fa0c8e5f 100644 --- a/lms/djangoapps/verify_student/api.py +++ b/lms/djangoapps/verify_student/api.py @@ -0,0 +1,35 @@ +""" +API module. +""" +from django.conf import settings +from django.utils.translation import gettext as _ + +from lms.djangoapps.verify_student.emails import send_verification_approved_email +from lms.djangoapps.verify_student.tasks import send_verification_status_email + + +def send_approval_email(attempt): + """ + Send an approval email to the learner associated with the IDV attempt. + """ + verification_status_email_vars = { + 'platform_name': settings.PLATFORM_NAME, + } + + expiration_datetime = attempt.expiration_datetime.date() + if settings.VERIFY_STUDENT.get('USE_DJANGO_MAIL'): + verification_status_email_vars['expiration_datetime'] = expiration_datetime.strftime("%m/%d/%Y") + verification_status_email_vars['full_name'] = attempt.user.profile.name + subject = _("Your {platform_name} ID verification was approved!").format( + platform_name=settings.PLATFORM_NAME + ) + context = { + 'subject': subject, + 'template': 'emails/passed_verification_email.txt', + 'email': attempt.user.email, + 'email_vars': verification_status_email_vars + } + send_verification_status_email.delay(context) + else: + email_context = {'user': attempt.user, 'expiration_datetime': expiration_datetime.strftime("%m/%d/%Y")} + send_verification_approved_email(context=email_context) diff --git a/lms/djangoapps/verify_student/management/commands/approve_id_verifications.py b/lms/djangoapps/verify_student/management/commands/approve_id_verifications.py index 48ef6e6cf3a9..b87b2eee4559 100644 --- a/lms/djangoapps/verify_student/management/commands/approve_id_verifications.py +++ b/lms/djangoapps/verify_student/management/commands/approve_id_verifications.py @@ -11,6 +11,7 @@ from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.management.base import BaseCommand, CommandError +from lms.djangoapps.verify_student.api import send_approval_email from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.utils import earliest_allowed_verification_date @@ -125,8 +126,8 @@ def _approve_verifications_from_file(self, user_ids_file, batch_size, sleep_time def _approve_id_verifications(self, user_ids): """ - This command manually approves ID verification attempts for a provided set of learners whose ID verification - attempt is in the submitted or must_retry state. + This method manually approves ID verification attempts for a provided set of user IDs so long as the attempt + is in the submitted or must_retry state. This method also send an IDV approval email to the user. Arguments: user_ids (list): user IDs of the users whose ID verification attempt should be manually approved @@ -148,5 +149,6 @@ def _approve_id_verifications(self, user_ids): for verification in existing_id_verifications: verification.approve(service='idv_verifications command') + send_approval_email(verification) return list(failed_user_ids) diff --git a/lms/djangoapps/verify_student/management/commands/tests/test_approve_id_verifications.py b/lms/djangoapps/verify_student/management/commands/tests/test_approve_id_verifications.py index 90947a2b0771..e6e580c1d1a6 100644 --- a/lms/djangoapps/verify_student/management/commands/tests/test_approve_id_verifications.py +++ b/lms/djangoapps/verify_student/management/commands/tests/test_approve_id_verifications.py @@ -8,6 +8,7 @@ import tempfile import pytest +from django.core import mail from django.core.management import CommandError, call_command from django.test import TestCase from testfixtures import LogCapture @@ -70,6 +71,35 @@ def test_approve_id_verifications(self, status): assert SoftwareSecurePhotoVerification.objects.filter(status='approved').count() == 3 + @ddt.data('submitted', 'must_retry') + def test_approve_id_verifications_email(self, status): + """ + Tests that the approve_id_verifications management command correctly sends approval emails. + """ + # Create SoftwareSecurePhotoVerification instances for the users. + for user in [self.user1_profile, self.user2_profile]: + SoftwareSecurePhotoVerification.objects.create( + user=user.user, + name=user.name, + status=status, + ) + SoftwareSecurePhotoVerification.objects.create( + user=self.user3_profile.user, + name=self.user3_profile.name, + status='denied', + ) + + call_command('approve_id_verifications', self.tmp_file_path) + + assert len(mail.outbox) == 2 + + # All three emails should have equal expiration dates, so just pick one from an attempt. + expiration_date = SoftwareSecurePhotoVerification.objects.first().expiration_datetime + for email in mail.outbox: + assert email.subject == 'Your édX ID verification was approved!' + assert 'Your édX ID verification photos have been approved' in email.body + assert expiration_date.strftime("%m/%d/%Y") in email.body + def test_user_does_not_exist_log(self): """ Tests that the approve_id_verifications management command logs an error when an invalid user ID is diff --git a/lms/djangoapps/verify_student/tests/test_api.py b/lms/djangoapps/verify_student/tests/test_api.py new file mode 100644 index 000000000000..acdebaa70c1c --- /dev/null +++ b/lms/djangoapps/verify_student/tests/test_api.py @@ -0,0 +1,43 @@ +""" +Tests of API module. +""" +from unittest.mock import patch + +import ddt +from django.conf import settings +from django.core import mail +from django.test import TestCase + +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.verify_student.api import send_approval_email +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification + + +@ddt.ddt +class TestSendApprovalEmail(TestCase): + """ + Test cases for the send_approval_email API method. + """ + def setUp(self): + super().setUp() + + self.user = UserFactory.create() + self.attempt = SoftwareSecurePhotoVerification( + status="submitted", + user=self.user + ) + self.attempt.save() + + def _assert_verification_approved_email(self, expiration_date): + """Check that a verification approved email was sent.""" + assert len(mail.outbox) == 1 + email = mail.outbox[0] + assert email.subject == 'Your édX ID verification was approved!' + assert 'Your édX ID verification photos have been approved' in email.body + assert expiration_date.strftime("%m/%d/%Y") in email.body + + @ddt.data(True, False) + def test_send_approval(self, use_ace): + with patch.dict(settings.VERIFY_STUDENT, {'USE_DJANGO_MAIL': use_ace}): + send_approval_email(self.attempt) + self._assert_verification_approved_email(self.attempt.expiration_datetime) diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 06e287d7314b..8c1912d9c2f8 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -36,7 +36,8 @@ from common.djangoapps.util.json_request import JsonResponse from common.djangoapps.util.views import require_global_staff from lms.djangoapps.commerce.utils import EcommerceService, is_account_activation_requirement_disabled -from lms.djangoapps.verify_student.emails import send_verification_approved_email, send_verification_confirmation_email +from lms.djangoapps.verify_student.api import send_approval_email +from lms.djangoapps.verify_student.emails import send_verification_confirmation_email from lms.djangoapps.verify_student.image import InvalidImageData, decode_image_data from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline from lms.djangoapps.verify_student.tasks import send_verification_status_email @@ -1117,24 +1118,7 @@ def results_callback(request): # lint-amnesty, pylint: disable=too-many-stateme log.info("[COSMO-184] Approved verification for receipt_id={receipt_id}.".format(receipt_id=receipt_id)) attempt.approve() - expiration_datetime = attempt.expiration_datetime.date() - if settings.VERIFY_STUDENT.get('USE_DJANGO_MAIL'): - verification_status_email_vars['expiration_datetime'] = expiration_datetime.strftime("%m/%d/%Y") - verification_status_email_vars['full_name'] = user.profile.name - subject = _("Your {platform_name} ID verification was approved!").format( - platform_name=settings.PLATFORM_NAME - ) - context = { - 'subject': subject, - 'template': 'emails/passed_verification_email.txt', - 'email': user.email, - 'email_vars': verification_status_email_vars - } - send_verification_status_email.delay(context) - else: - email_context = {'user': user, 'expiration_datetime': expiration_datetime.strftime("%m/%d/%Y")} - send_verification_approved_email(context=email_context) - + send_approval_email(attempt) elif result == "FAIL": log.debug("Denying verification for %s", receipt_id)