From dacfcc985feac678f06656cad2bcde3a1ba45b5c Mon Sep 17 00:00:00 2001 From: Mushtaq Ali <mushtaque@edx.org> Date: Wed, 31 Aug 2016 16:55:07 +0500 Subject: [PATCH] Generate certificates for verified users with audit certificate statues - ECOM-5012 --- common/djangoapps/student/models.py | 17 +++++++++++------ lms/djangoapps/instructor/tests/test_certificates.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lms/djangoapps/instructor/views/api.py | 8 +++++++- lms/djangoapps/instructor/views/instructor_dashboard.py | 6 ++++++ lms/djangoapps/instructor_task/api.py | 7 ++++++- lms/djangoapps/instructor_task/tasks_helper.py | 4 ++++ lms/templates/instructor/instructor_dashboard_2/certificates.html | 6 ++++++ 7 files changed, 111 insertions(+), 8 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 8b1c259..6a5adc3 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -943,12 +943,17 @@ class CourseEnrollmentManager(models.Manager): return is_course_full - def users_enrolled_in(self, course_id): - """Return a queryset of User for every user enrolled in the course.""" - return User.objects.filter( - courseenrollment__course_id=course_id, - courseenrollment__is_active=True - ) + def users_enrolled_in(self, course_id, mode=None): + """ + Returns a queryset of User for every user enrolled in the course. + + course_id (CourseKey): The key of the course associated with the enrollment. + mode (String): The enrolled mode of the users. + """ + _query = {'courseenrollment__course_id': course_id, 'courseenrollment__is_active': True} + if mode: + _query['courseenrollment__mode'] = mode + return User.objects.filter(**_query) def enrollment_counts(self, course_id): """ diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 82ffb70..a1d5a1a 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -3,12 +3,19 @@ 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 mock import patch, PropertyMock + +from course_modes.models import CourseMode from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from config_models.models import cache @@ -344,6 +351,70 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase): u'the "Pending Tasks" section.' ) + @patch( + 'lms.djangoapps.grades.new.course_grade.CourseGrade.summary', + PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75}) + ) + @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1)) + @ddt.data( + ('verified', 'ID Verified', True), + ('unverified', 'Not ID Verified', False) + ) + @ddt.unpack + def test_verified_users_with_audit_certs(self, expected_cert_status, verification_output, user_verified): + """ + Test that `verified_users_with_audit_certs` option regenerates certificate for verified users with audit + certificates get certificate. + + Scenario: + User enrolled in course as audit, + User passed the course as audit so they have `audit_passing` certificate status, + User switched to verified mode and is ID verified, + Regenerate certificates for `verified_users_with_audit_certs` is run, + Modified certificate status is `verified` 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.assertEqual(enrollment.mode, CourseMode.AUDIT) + + # Generate certificate for user and check that user has a audit passing certificate. + with patch('student.models.CourseEnrollment.refund_cutoff_date') as cutoff_date: + cutoff_date.return_value = datetime.now(pytz.UTC) - timedelta(minutes=5) + cert_status = certs_api.generate_user_certificates(student=self.user, course_key=self.course.id, course=self.course) + self.assertEqual(cert_status, CertificateStatuses.audit_passing) + + # Update user enrollment mode to verified mode. + enrollment.update_enrollment(mode='verified') + self.assertEqual(enrollment.mode, CourseMode.VERIFIED) + + with patch( + 'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified' + ) as user_verify: + user_verify.return_value = user_verified + + # Login the client and access the url with 'certificate_statuses' + self.client.login(username=self.global_staff.username, password='test') + url = reverse('start_certificate_regeneration', kwargs={'course_id': unicode(self.course.id)}) + response = self.client.post(url, data={'certificate_statuses': ['verified_users_with_audit_certs']}) + + # Assert 200 status code in response + self.assertEqual(response.status_code, 200) + res_json = json.loads(response.content) + + # Assert request is successful + self.assertTrue(res_json['success']) + + # Assert success message + self.assertEqual( + res_json['message'], + u'Certificate regeneration task has been started. You can view the status of the generation task in ' + u'the "Pending Tasks" section.' + ) + # Check user has a not audit passing certificate now. + cert = certs_api.get_certificate_for_user(self.user.username, self.course.id) + self.assertNotEqual(cert['status'], CertificateStatuses.audit_passing) + self.assertEqual(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 8b27e24..d4647dc 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, + # verified users with audit passing and not passing certificate statuses. + 'verified_users_with_audit_certs' + ] if not set(certificates_statuses).issubset(allowed_statuses): return JsonResponse( {'message': _('Please select certificate statuses from the list only.')}, diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 8c3327f..8621e7a 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -343,6 +343,11 @@ def _section_certificates(course): for certificate in GeneratedCertificate.get_unique_statuses(course_key=course.id) } + # Get the count of all course verified users with audit passing and audit not passing statuses. + verified_users_with_audit_certs = CourseEnrollment.objects.users_enrolled_in(course.id, mode='verified').filter( + generatedcertificate__status__in=[CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing] + ).count() + return { 'section_key': 'certificates', 'section_display_name': _('Certificates'), @@ -354,6 +359,7 @@ def _section_certificates(course): 'html_cert_enabled': html_cert_enabled, 'active_certificate': certs_api.get_active_web_certificate(course), 'certificate_statuses_with_count': certificate_statuses_with_count, + 'verified_users_with_audit_certs': verified_users_with_audit_certs, 'status': CertificateStatuses, 'certificate_generation_history': CertificateGenerationHistory.objects.filter(course_id=course.id).order_by("-created"), diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index a412daf..79b8ecc 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -33,7 +33,7 @@ from lms.djangoapps.instructor_task.tasks import ( export_ora2_data, ) -from certificates.models import CertificateGenerationHistory +from certificates.models import CertificateGenerationHistory, CertificateStatuses from lms.djangoapps.instructor_task.api_helper import ( check_arguments_for_rescoring, @@ -507,6 +507,11 @@ def regenerate_certificates(request, course_key, statuses_to_regenerate): task_type = 'regenerate_certificates_all_student' task_input = {} + # Update task_input for verified users with audit passing and not passing certificate statuses. + if 'verified_users_with_audit_certs' in statuses_to_regenerate: + task_input.update({"student_set": 'verified_users_with_audit_certs'}) + statuses_to_regenerate = [CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing] + task_input.update({"statuses_to_regenerate": statuses_to_regenerate}) task_class = generate_certificates task_key = "" diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index bd05443..ea3102e 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -1429,6 +1429,10 @@ def generate_students_certificates( specific_student_id = task_input.get('specific_student_id') students_to_generate_certs_for = students_to_generate_certs_for.filter(id=specific_student_id) + # Verified users with audit passing and not passing certificate statuses. + elif student_set == "verified_users_with_audit_certs": + students_to_generate_certs_for = CourseEnrollment.objects.users_enrolled_in(course_id, mode='verified') + task_progress = TaskProgress(action_name, students_to_generate_certs_for.count(), start_time) current_step = {'step': 'Calculating students already have certificates'} diff --git a/lms/templates/instructor/instructor_dashboard_2/certificates.html b/lms/templates/instructor/instructor_dashboard_2/certificates.html index e84c47a..e4efe57 100644 --- a/lms/templates/instructor/instructor_dashboard_2/certificates.html +++ b/lms/templates/instructor/instructor_dashboard_2/certificates.html @@ -130,6 +130,12 @@ from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_str </div> <div> <label> + <input id="certificate_status_verified_users_with_audit_certs}" type="checkbox" name="certificate_statuses" value="verified_users_with_audit_certs"> + ${_("Regenerate for verified learners with audit certificates. ({count})").format(count=section_data['verified_users_with_audit_certs'])} + </label> + </div> + <div> + <label> <input id="certificate_status_${section_data['status'].error}" type="checkbox" name="certificate_statuses" value="${section_data['status'].error}"> ${_("Regenerate for learners in an error state. ({count})").format(count=section_data['certificate_statuses_with_count'].get(section_data['status'].error, 0))} </label> -- libgit2 0.26.0