diff --git a/lms/djangoapps/certificates/signals.py b/lms/djangoapps/certificates/signals.py index b116c86f53b56b0bf955549c288dc3a18fd1487f..40159eaa026fe4af18463e0edb20a23092771ca0 100644 --- a/lms/djangoapps/certificates/signals.py +++ b/lms/djangoapps/certificates/signals.py @@ -7,8 +7,10 @@ import logging from django.db.models.signals import post_save from django.dispatch import receiver +from common.djangoapps.course_modes import api as modes_api from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED from lms.djangoapps.certificates.generation_handler import ( generate_allowlist_certificate_task, is_using_certificate_allowlist_and_is_on_allowlist @@ -145,6 +147,22 @@ def _listen_for_id_verification_status_changed(sender, user, **kwargs): # pylin )) +@receiver(ENROLLMENT_TRACK_UPDATED) +def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs): # pylint: disable=unused-argument + """ + Listen for the signal indicating that a user's enrollment mode has changed. + + If possible, grant the user a course certificate. Note that we intentionally do not revoke certificates here, even + if the user has moved to the audit track. + """ + if modes_api.is_eligible_for_certificate(mode): + if is_using_certificate_allowlist_and_is_on_allowlist(user, course_key): + log.info(f'{course_key} is using allowlist certificates, and the user {user.id} is on its allowlist. ' + f'Attempt will be made to generate an allowlist certificate since the enrollment mode is now ' + f'{mode}.') + generate_allowlist_certificate_task(user, course_key) + + def _fire_ungenerated_certificate_task(user, course_key, expected_verification_status=None): """ Helper function to fire certificate generation task. diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index d6e57b5db551c52f037ceeffa29c07dc4ea56903..6be117f8990b064db43c75c3faf6fab047c93450 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -58,9 +58,9 @@ class SelfGeneratedCertsSignalTest(ModuleStoreTestCase): assert not cert_generation_enabled(course.id) -class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase): +class AllowlistGeneratedCertificatesTest(ModuleStoreTestCase): """ - Tests for whitelisted student auto-certificate generation + Tests for allowlisted student auto-certificate generation """ def setUp(self): @@ -573,3 +573,63 @@ class CertificateGenerationTaskTest(ModuleStoreTestCase): _fire_ungenerated_certificate_task(self.user, self.course.id) task_created = mock_generate_certificate_apply_async.called assert task_created == should_create + + +@override_waffle_flag(CERTIFICATES_USE_ALLOWLIST, active=True) +@override_waffle_flag(AUTO_CERTIFICATE_GENERATION_SWITCH, active=True) +class EnrollmentModeChangeCertsTest(ModuleStoreTestCase): + """ + Tests for certificate generation task firing when the user's enrollment mode changes + """ + def setUp(self): + super().setUp() + self.user = UserFactory.create() + self.verified_course = CourseFactory.create( + self_paced=True, + ) + self.verified_course_key = self.verified_course.id # pylint: disable=no-member + self.verified_enrollment = CourseEnrollmentFactory( + user=self.user, + course_id=self.verified_course_key, + is_active=True, + mode='verified', + ) + CertificateWhitelistFactory( + user=self.user, + course_id=self.verified_course_key + ) + + self.audit_course = CourseFactory.create(self_paced=False) + self.audit_course_key = self.audit_course.id # pylint: disable=no-member + self.audit_enrollment = CourseEnrollmentFactory( + user=self.user, + course_id=self.audit_course_key, + is_active=True, + mode='audit', + ) + CertificateWhitelistFactory( + user=self.user, + course_id=self.audit_course_key + ) + + def test_audit_to_verified(self): + """ + Test that we try to generate a certificate when the user switches from audit to verified + """ + with mock.patch( + 'lms.djangoapps.certificates.signals.generate_allowlist_certificate_task', + return_value=None + ) as mock_allowlist_task: + self.audit_enrollment.change_mode('verified') + mock_allowlist_task.assert_called_with(self.user, self.audit_course_key) + + def test_verified_to_audit(self): + """ + Test that we do not try to generate a certificate when the user switches from verified to audit + """ + with mock.patch( + 'lms.djangoapps.certificates.signals.generate_allowlist_certificate_task', + return_value=None + ) as mock_allowlist_task: + self.verified_enrollment.change_mode('audit') + mock_allowlist_task.assert_not_called()