diff --git a/lms/djangoapps/verify_student/signals.py b/lms/djangoapps/verify_student/signals.py index 3e80a44fa3..022269e4b9 100644 --- a/lms/djangoapps/verify_student/signals.py +++ b/lms/djangoapps/verify_student/signals.py @@ -3,7 +3,9 @@ Signal handler for setting default course verification dates """ +from django.db.models.signals import post_save from django.core.exceptions import ObjectDoesNotExist +from django.dispatch import Signal from django.dispatch.dispatcher import receiver from openedx.core.djangoapps.user_api.accounts.signals import USER_RETIRE_LMS_CRITICAL @@ -12,6 +14,10 @@ from xmodule.modulestore.django import SignalHandler, modulestore from .models import SoftwareSecurePhotoVerification, VerificationDeadline +# Signal for emitting IDV submission and review updates +idv_update_signal = Signal(providing_args=["attempt_id", "user_id", "status", "full_name", "profile_name"]) + + @receiver(SignalHandler.course_published) def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument """ @@ -32,3 +38,21 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable def _listen_for_lms_retire(sender, **kwargs): # pylint: disable=unused-argument user = kwargs.get('user') SoftwareSecurePhotoVerification.retire_user(user.id) + + +@receiver(post_save, sender=SoftwareSecurePhotoVerification) +def send_idv_update(sender, instance, **kwargs): # pylint: disable=unused-argument + """ + Catches the post save signal from the SoftwareSecurePhotoVerification model, and emits + another signal with limited information from the model. We are choosing to re-emit a signal + as opposed to relying only on the post_save signal to avoid the chance that other apps + import the SoftwareSecurePhotoVerification model. + """ + idv_update_signal.send( + sender='idv_update', + attempt_id=instance.id, + user_id=instance.user.id, + status=instance.status, + full_name=instance.name, + profile_name=instance.user.profile.name + ) diff --git a/lms/djangoapps/verify_student/tests/test_signals.py b/lms/djangoapps/verify_student/tests/test_signals.py index 1c9888fbcf..4621691ac4 100644 --- a/lms/djangoapps/verify_student/tests/test_signals.py +++ b/lms/djangoapps/verify_student/tests/test_signals.py @@ -6,6 +6,7 @@ Unit tests for the VerificationDeadline signals from datetime import timedelta from django.utils.timezone import now +from unittest.mock import patch from common.djangoapps.student.tests.factories import UserFactory from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline @@ -102,3 +103,46 @@ class RetirementSignalTest(ModuleStoreTestCase): # All values for this user should now be empty string for field in ('name', 'face_image_url', 'photo_id_image_url', 'photo_id_key'): assert '' == getattr(ver_obj, field) + + +class PostSavePhotoVerificationTest(ModuleStoreTestCase): + """ + Tests for the post_save signal on the SoftwareSecurePhotoVerification model. + This receiver should emit another signal that contains limited data about + the verification attempt that was updated. + """ + + @patch('lms.djangoapps.verify_student.signals.idv_update_signal.send') + def test_post_save_signal(self, mock_signal): + user = UserFactory.create() + + # create new softwaresecureverification + attempt = SoftwareSecurePhotoVerification.objects.create( + user=user, + name='Bob Doe', + face_image_url='https://test.face', + photo_id_image_url='https://test.photo', + photo_id_key='test+key' + ) + self.assertTrue(mock_signal.called) + mock_signal.assert_called_with( + sender='idv_update', + attempt_id=attempt.id, + user_id=attempt.user.id, + status=attempt.status, + full_name=attempt.name, + profile_name=attempt.user.profile.name + ) + mock_signal.reset_mock() + + attempt.mark_ready() + + self.assertTrue(mock_signal.called) + mock_signal.assert_called_with( + sender='idv_update', + attempt_id=attempt.id, + user_id=attempt.user.id, + status=attempt.status, + full_name=attempt.name, + profile_name=attempt.user.profile.name + )