diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index f168c93dba..c0de5d495d 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -235,6 +235,7 @@ class XQueueCertInterface(object): status.auditing, status.audit_passing, status.audit_notpassing, + status.unverified, ] cert_status = certificate_status_for_student(student, course_id)['status'] diff --git a/lms/djangoapps/certificates/signals.py b/lms/djangoapps/certificates/signals.py index 44b1680925..1f66adf2d7 100644 --- a/lms/djangoapps/certificates/signals.py +++ b/lms/djangoapps/certificates/signals.py @@ -6,10 +6,12 @@ import logging from django.db.models.signals import post_save from django.dispatch import receiver -from certificates.models import \ - CertificateGenerationCourseSetting, \ - CertificateWhitelist, \ +from certificates.models import ( + CertificateGenerationCourseSetting, + CertificateWhitelist, + CertificateStatuses, GeneratedCertificate +) from certificates.tasks import generate_certificate from courseware import courses from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory @@ -97,10 +99,16 @@ def _listen_for_track_change(sender, user, **kwargs): # pylint: disable=unused- def fire_ungenerated_certificate_task(user, course_key): """ Helper function to fire un-generated certificate tasks - :param user: A User object. - :param course_id: A CourseKey object. + + The 'mode_is_verified' query is copied from the GeneratedCertificate model, + but is done here in an attempt to reduce traffic to the workers. + If the learner is verified and their cert has the 'unverified' status, + we regenerate the cert. """ - if GeneratedCertificate.certificate_for_student(user, course_key) is None: + enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_key) + mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES + cert = GeneratedCertificate.certificate_for_student(user, course_key) + if mode_is_verified and (cert is None or cert.status == 'unverified'): generate_certificate.apply_async(kwargs={ 'student': unicode(user.id), 'course_key': unicode(course_key), diff --git a/lms/djangoapps/certificates/tests/test_api.py b/lms/djangoapps/certificates/tests/test_api.py index aafe6b10f9..4a12529528 100644 --- a/lms/djangoapps/certificates/tests/test_api.py +++ b/lms/djangoapps/certificates/tests/test_api.py @@ -520,8 +520,11 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu def test_generate_user_certificates_with_unverified_cert_status(self): """ - Generate user certificate will not raise exception in case of certificate is None. + Generate user certificate when the certificate is unverified + will trigger an update to the certificate if the user has since + verified. """ + self._setup_course_certificate() # generate certificate with unverified status. GeneratedCertificateFactory.create( user=self.student, @@ -531,9 +534,9 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu ) with mock_passing_grade(): - with self._mock_queue(is_successful=False): + with self._mock_queue(): status = certs_api.generate_user_certificates(self.student, self.course.id) - self.assertEqual(status, None) + self.assertEqual(status, 'generating') @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) def test_new_cert_requests_returns_generating_for_html_certificate(self): diff --git a/lms/djangoapps/certificates/tests/test_signals.py b/lms/djangoapps/certificates/tests/test_signals.py index b006e8c349..a6f76a91af 100644 --- a/lms/djangoapps/certificates/tests/test_signals.py +++ b/lms/djangoapps/certificates/tests/test_signals.py @@ -61,7 +61,19 @@ class WhitelistGeneratedCertificatesTest(ModuleStoreTestCase): super(WhitelistGeneratedCertificatesTest, self).setUp() self.course = CourseFactory.create(self_paced=True) self.user = UserFactory.create() + CourseEnrollmentFactory( + user=self.user, + course_id=self.course.id, + is_active=True, + mode="verified", + ) self.ip_course = CourseFactory.create(self_paced=False) + CourseEnrollmentFactory( + user=self.user, + course_id=self.ip_course.id, + is_active=True, + mode="verified", + ) def test_cert_generation_on_whitelist_append_self_paced(self): """ @@ -137,6 +149,11 @@ class PassingGradeCertsTest(ModuleStoreTestCase): is_active=True, mode="verified", ) + attempt = SoftwareSecurePhotoVerification.objects.create( + user=self.user, + status='submitted' + ) + attempt.approve() def test_cert_generation_on_passing_self_paced(self): with mock.patch( @@ -204,7 +221,7 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase): user=self.user_one, course_id=self.course_one.id, is_active=True, - mode='honor', + mode='verified', ) self.user_two = UserFactory.create() self.course_two = CourseFactory.create(self_paced=False) @@ -212,18 +229,18 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase): user=self.user_two, course_id=self.course_two.id, is_active=True, - mode='honor' + mode='verified' ) + with mock_passing_grade(): + grade_factory = CourseGradeFactory() + grade_factory.update(self.user_one, self.course_one) + grade_factory.update(self.user_two, self.course_two) def test_cert_generation_on_photo_verification_self_paced(self): with mock.patch( 'lms.djangoapps.certificates.signals.generate_certificate.apply_async', return_value=None ) as mock_generate_certificate_apply_async: - with mock_passing_grade(): - grade_factory = CourseGradeFactory() - grade_factory.update(self.user_one, self.course_one) - with waffle.waffle().override(waffle.SELF_PACED_ONLY, active=True): mock_generate_certificate_apply_async.assert_not_called() attempt = SoftwareSecurePhotoVerification.objects.create( @@ -241,10 +258,6 @@ class LearnerTrackChangeCertsTest(ModuleStoreTestCase): 'lms.djangoapps.certificates.signals.generate_certificate.apply_async', return_value=None ) as mock_generate_certificate_apply_async: - with mock_passing_grade(): - grade_factory = CourseGradeFactory() - grade_factory.update(self.user_two, self.course_two) - with waffle.waffle().override(waffle.INSTRUCTOR_PACED_ONLY, active=True): mock_generate_certificate_apply_async.assert_not_called() attempt = SoftwareSecurePhotoVerification.objects.create( diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 2609d293f4..33b62d3c50 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -1007,8 +1007,9 @@ def _get_cert_data(student, course, course_key, is_active, enrollment_mode): # If the learner is in verified modes and the student did not have # their ID verified, we need to show message to ask learner to verify their ID first - missing_required_verification = enrollment_mode in CourseMode.VERIFIED_MODES and \ - not SoftwareSecurePhotoVerification.user_is_verified(student) + missing_required_verification = ( + enrollment_mode in CourseMode.VERIFIED_MODES and not SoftwareSecurePhotoVerification.user_is_verified(student) + ) if missing_required_verification or cert_downloadable_status['is_unverified']: platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)