diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index f87b5c7b5d..a375b980a7 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -95,7 +95,9 @@ class CertificateStatuses(object): readable_statuses = { downloadable: "already received", notpassing: "didn't receive", - error: "error states" + error: "error states", + audit_passing: "audit passing states", + audit_notpassing: "audit not passing states", } PASSED_STATUSES = (downloadable, generating) diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 82ffb70641..14e404af84 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -3,12 +3,21 @@ import contextlib import ddt import mock import json +import pytz + +from datetime import datetime, timedelta from nose.plugins.attrib import attr from django.core.urlresolvers import reverse from django.core.exceptions import ObjectDoesNotExist from django.test.utils import override_settings from django.conf import settings + +from course_modes.models import CourseMode +from capa.xqueue_interface import XQueueInterface +from lms.djangoapps.grades.tests.utils import mock_passing_grade +from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory +from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from config_models.models import cache @@ -344,6 +353,85 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase): u'the "Pending Tasks" section.' ) + @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) + @ddt.data( + (CertificateStatuses.generating, 'ID Verified', 'approved'), + (CertificateStatuses.unverified, 'Not ID Verified', 'denied'), + ) + @ddt.unpack + def test_verified_users_with_audit_certs(self, expected_cert_status, verification_output, id_verification_status): + """ + Test certificate regeneration for verified users with audit certificates. + + Scenario: + Enroll user in a course in audit mode, + User passed the course and now he has `audit_passing` certificate status, + User switched to verified mode and is ID verified, + Regenerate certificate for it, + Modified certificate status is `generating` if user is ID verified otherwise `unverified`. + """ + # Check that user is enrolled in audit mode. + enrollment = CourseEnrollment.get_enrollment(self.user, self.course.id) + self.assertEquals(enrollment.mode, CourseMode.AUDIT) + + with mock_passing_grade(): + # Generate certificate for user and check that user has a audit passing certificate. + cert_status = certs_api.generate_user_certificates( + student=self.user, + course_key=self.course.id, + course=self.course, + ) + + # Check that certificate status is 'audit_passing'. + self.assertEquals(cert_status, CertificateStatuses.audit_passing) + + # Update user enrollment mode to verified mode. + enrollment.update_enrollment(mode=CourseMode.VERIFIED) + self.assertEquals(enrollment.mode, CourseMode.VERIFIED) + + # Create and assert user's ID verification record. + SoftwareSecurePhotoVerificationFactory.create(user=self.user, status=id_verification_status) + actual_verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( + self.user, + self.course.id, + enrollment.mode, + ) + self.assertEquals(actual_verification_status, verification_output) + + # Login the client and access the url with 'audit_passing' status. + self.client.login(username=self.global_staff.username, password='test') + url = reverse( + 'start_certificate_regeneration', + kwargs={'course_id': unicode(self.course.id)} + ) + + with mock.patch.object(XQueueInterface, 'send_to_queue') as mock_send: + mock_send.return_value = (0, None) + response = self.client.post( + url, + {'certificate_statuses': [CertificateStatuses.audit_passing]} + ) + + # Assert 200 status code in response + self.assertEquals(response.status_code, 200) + res_json = json.loads(response.content) + + # Assert request is successful + self.assertTrue(res_json['success']) + + # Assert success message + self.assertEquals( + res_json['message'], + u'Certificate regeneration task has been started. ' + u'You can view the status of the generation task in ' + u'the "Pending Tasks" section.' + ) + + # Now, check whether user has audit certificate. + cert = certs_api.get_certificate_for_user(self.user.username, self.course.id) + self.assertNotEquals(cert['status'], CertificateStatuses.audit_passing) + self.assertEquals(cert['status'], expected_cert_status) + def test_certificate_regeneration_error(self): """ Test certificate regeneration errors out when accessed with either empty list of 'certificate_statuses' or diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 8b27e24b49..fe3484cf9c 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2862,7 +2862,13 @@ def start_certificate_regeneration(request, course_id): ) # Check if the selected statuses are allowed - allowed_statuses = [CertificateStatuses.downloadable, CertificateStatuses.error, CertificateStatuses.notpassing] + allowed_statuses = [ + CertificateStatuses.downloadable, + CertificateStatuses.error, + CertificateStatuses.notpassing, + CertificateStatuses.audit_passing, + CertificateStatuses.audit_notpassing, + ] if not set(certificates_statuses).issubset(allowed_statuses): return JsonResponse( {'message': _('Please select certificate statuses from the list only.')}, diff --git a/lms/templates/instructor/instructor_dashboard_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/certificates.html index e84c47ae6e..ce9e955289 100644 --- a/lms/templates/instructor/instructor_dashboard_2/certificates.html +++ b/lms/templates/instructor/instructor_dashboard_2/certificates.html @@ -128,6 +128,18 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str ${_("Regenerate for learners who have not received certificates. ({count})").format(count=section_data['certificate_statuses_with_count'].get(section_data['status'].notpassing, 0))} +