Added ability to regenerate certificates from Instructor Dashboard
This commit is contained in:
@@ -54,6 +54,7 @@ import os
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Count
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
@@ -187,6 +188,31 @@ class GeneratedCertificate(models.Model):
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get_unique_statuses(cls, course_key=None, flat=False):
|
||||
"""
|
||||
1 - Return unique statuses as a list of dictionaries containing the following key value pairs
|
||||
[
|
||||
{'status': 'status value from db', 'count': 'occurrence count of the status'},
|
||||
{...},
|
||||
..., ]
|
||||
|
||||
2 - if flat is 'True' then return unique statuses as a list
|
||||
3 - if course_key is given then return unique statuses associated with the given course
|
||||
|
||||
:param course_key: Course Key identifier
|
||||
:param flat: boolean showing whether to return statuses as a list of values or a list of dictionaries.
|
||||
"""
|
||||
query = cls.objects
|
||||
|
||||
if course_key:
|
||||
query = query.filter(course_id=course_key)
|
||||
|
||||
if flat:
|
||||
return query.values_list('status', flat=True).distinct()
|
||||
else:
|
||||
return query.values('status').annotate(count=Count('status'))
|
||||
|
||||
|
||||
@receiver(post_save, sender=GeneratedCertificate)
|
||||
def handle_post_cert_generated(sender, instance, **kwargs): # pylint: disable=no-self-argument, unused-argument
|
||||
|
||||
@@ -12,7 +12,8 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from config_models.models import cache
|
||||
from courseware.tests.factories import GlobalStaffFactory, InstructorFactory, UserFactory
|
||||
from certificates.models import CertificateGenerationConfiguration
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.models import CertificateGenerationConfiguration, CertificateStatuses
|
||||
from certificates import api as certs_api
|
||||
|
||||
|
||||
@@ -486,3 +487,78 @@ class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertTrue(res_json['success'])
|
||||
|
||||
def test_certificate_regeneration_success(self):
|
||||
"""
|
||||
Test certificate regeneration is successful when accessed with 'certificate_statuses'
|
||||
present in GeneratedCertificate table.
|
||||
"""
|
||||
|
||||
# Create a generated Certificate of some user with status 'downloadable'
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# 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': [CertificateStatuses.downloadable]})
|
||||
|
||||
# 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.'
|
||||
)
|
||||
|
||||
def test_certificate_regeneration_error(self):
|
||||
"""
|
||||
Test certificate regeneration errors out when accessed with either empty list of 'certificate_statuses' or
|
||||
the 'certificate_statuses' that are not present in GeneratedCertificate table.
|
||||
"""
|
||||
# Create a dummy course and GeneratedCertificate with the same status as the one we will use to access
|
||||
# 'start_certificate_regeneration' but their error message should be displayed as GeneratedCertificate
|
||||
# belongs to a different course
|
||||
dummy_course = CourseFactory.create()
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=dummy_course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# Login the client and access the url without '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)
|
||||
|
||||
# Assert 400 status code in response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
res_json = json.loads(response.content)
|
||||
|
||||
# Assert Error Message
|
||||
self.assertEqual(
|
||||
res_json['message'],
|
||||
u'Please select one or more certificate statuses that require certificate regeneration.'
|
||||
)
|
||||
|
||||
# Access the url passing 'certificate_statuses' that are not present in db
|
||||
url = reverse('start_certificate_regeneration', kwargs={'course_id': unicode(self.course.id)})
|
||||
response = self.client.post(url, data={'certificate_statuses': [CertificateStatuses.generating]})
|
||||
|
||||
# Assert 400 status code in response
|
||||
self.assertEqual(response.status_code, 400)
|
||||
res_json = json.loads(response.content)
|
||||
|
||||
# Assert Error Message
|
||||
self.assertEqual(res_json['message'], u'Please select certificate statuses from the list only.')
|
||||
|
||||
@@ -92,7 +92,7 @@ from instructor.views import INVOICE_KEY
|
||||
from submissions import api as sub_api # installed from the edx-submissions repository
|
||||
|
||||
from certificates import api as certs_api
|
||||
from certificates.models import CertificateWhitelist
|
||||
from certificates.models import CertificateWhitelist, GeneratedCertificate
|
||||
|
||||
from bulk_email.models import CourseEmail
|
||||
from student.models import get_user_by_username_or_email
|
||||
@@ -2708,6 +2708,43 @@ def start_certificate_generation(request, course_id):
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_global_staff
|
||||
@require_POST
|
||||
def start_certificate_regeneration(request, course_id):
|
||||
"""
|
||||
Start regenerating certificates for students whose certificate statuses lie with in 'certificate_statuses'
|
||||
entry in POST data.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
certificates_statuses = request.POST.getlist('certificate_statuses', [])
|
||||
if not certificates_statuses:
|
||||
return JsonResponse(
|
||||
{'message': _('Please select one or more certificate statuses that require certificate regeneration.')},
|
||||
status=400
|
||||
)
|
||||
|
||||
# Check if the selected statuses are allowed
|
||||
allowed_statuses = GeneratedCertificate.get_unique_statuses(course_key=course_key, flat=True)
|
||||
if not set(certificates_statuses).issubset(allowed_statuses):
|
||||
return JsonResponse(
|
||||
{'message': _('Please select certificate statuses from the list only.')},
|
||||
status=400
|
||||
)
|
||||
try:
|
||||
instructor_task.api.regenerate_certificates(request, course_key, certificates_statuses)
|
||||
except AlreadyRunningError as error:
|
||||
return JsonResponse({'message': error.message}, status=400)
|
||||
|
||||
response_payload = {
|
||||
'message': _('Certificate regeneration task has been started. '
|
||||
'You can view the status of the generation task in the "Pending Tasks" section.'),
|
||||
'success': True
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_global_staff
|
||||
|
||||
@@ -143,6 +143,10 @@ urlpatterns = patterns(
|
||||
'instructor.views.api.start_certificate_generation',
|
||||
name='start_certificate_generation'),
|
||||
|
||||
url(r'^start_certificate_regeneration',
|
||||
'instructor.views.api.start_certificate_regeneration',
|
||||
name='start_certificate_regeneration'),
|
||||
|
||||
url(r'^create_certificate_exception/(?P<white_list_student>[^/]*)',
|
||||
'instructor.views.api.create_certificate_exception',
|
||||
name='create_certificate_exception'),
|
||||
|
||||
@@ -37,7 +37,7 @@ from student.models import CourseEnrollment
|
||||
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
|
||||
from course_modes.models import CourseMode, CourseModesArchive
|
||||
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
|
||||
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist
|
||||
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist, GeneratedCertificate
|
||||
from certificates import api as certs_api
|
||||
from util.date_utils import get_default_time_display
|
||||
|
||||
@@ -299,6 +299,7 @@ def _section_certificates(course):
|
||||
'enabled_for_course': certs_api.cert_generation_enabled(course.id),
|
||||
'instructor_generation_enabled': instructor_generation_enabled,
|
||||
'html_cert_enabled': html_cert_enabled,
|
||||
'certificate_statuses': GeneratedCertificate.get_unique_statuses(course_key=course.id),
|
||||
'urls': {
|
||||
'generate_example_certificates': reverse(
|
||||
'generate_example_certificates',
|
||||
@@ -312,6 +313,10 @@ def _section_certificates(course):
|
||||
'start_certificate_generation',
|
||||
kwargs={'course_id': course.id}
|
||||
),
|
||||
'start_certificate_regeneration': reverse(
|
||||
'start_certificate_regeneration',
|
||||
kwargs={'course_id': course.id}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'list_instructor_tasks',
|
||||
kwargs={'course_id': course.id}
|
||||
|
||||
@@ -512,3 +512,27 @@ def generate_certificates_for_students(request, course_key, students=None): # p
|
||||
task_key = ""
|
||||
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
|
||||
def regenerate_certificates(request, course_key, statuses_to_regenerate, students=None):
|
||||
"""
|
||||
Submits a task to regenerate certificates for given students enrolled in the course or
|
||||
all students if argument 'students' is None.
|
||||
Regenerate Certificate only if the status of the existing generated certificate is in 'statuses_to_regenerate'
|
||||
list passed in the arguments.
|
||||
|
||||
Raises AlreadyRunningError if certificates are currently being generated.
|
||||
"""
|
||||
if students:
|
||||
task_type = 'regenerate_certificates_certain_student'
|
||||
students = [student.id for student in students]
|
||||
task_input = {'students': students}
|
||||
else:
|
||||
task_type = 'regenerate_certificates_all_student'
|
||||
task_input = {}
|
||||
|
||||
task_input.update({"statuses_to_regenerate": statuses_to_regenerate})
|
||||
task_class = generate_certificates
|
||||
task_key = ""
|
||||
|
||||
return submit_task(request, task_type, task_class, course_key, task_input, task_key)
|
||||
|
||||
@@ -1414,7 +1414,8 @@ def generate_students_certificates(
|
||||
current_step = {'step': 'Calculating students already have certificates'}
|
||||
task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
students_require_certs = students_require_certificate(course_id, enrolled_students)
|
||||
statuses_to_regenerate = task_input.get('statuses_to_regenerate', [])
|
||||
students_require_certs = students_require_certificate(course_id, enrolled_students, statuses_to_regenerate)
|
||||
|
||||
task_progress.skipped = task_progress.total - len(students_require_certs)
|
||||
|
||||
@@ -1523,15 +1524,31 @@ def cohort_students_and_upload(_xmodule_instance_args, _entry_id, course_id, tas
|
||||
return task_progress.update_task_state(extra_meta=current_step)
|
||||
|
||||
|
||||
def students_require_certificate(course_id, enrolled_students):
|
||||
""" Returns list of students where certificates needs to be generated.
|
||||
Removing those students who have their certificate already generated
|
||||
from total enrolled students for given course.
|
||||
def students_require_certificate(course_id, enrolled_students, statuses_to_regenerate=None):
|
||||
"""
|
||||
Returns list of students where certificates needs to be generated.
|
||||
if 'statuses_to_regenerate' is given then return students that have Generated Certificates
|
||||
and the generated certificate status lies in 'statuses_to_regenerate'
|
||||
|
||||
if 'statuses_to_regenerate' is not given then return all the enrolled student skipping the ones
|
||||
whose certificates have already been generated.
|
||||
|
||||
:param course_id:
|
||||
:param enrolled_students:
|
||||
:param statuses_to_regenerate:
|
||||
"""
|
||||
# compute those students where certificates already generated
|
||||
students_already_have_certs = User.objects.filter(
|
||||
~Q(generatedcertificate__status=CertificateStatuses.unavailable),
|
||||
generatedcertificate__course_id=course_id)
|
||||
return list(set(enrolled_students) - set(students_already_have_certs))
|
||||
if statuses_to_regenerate:
|
||||
# Return Students that have Generated Certificates and the generated certificate status
|
||||
# lies in 'statuses_to_regenerate'
|
||||
return User.objects.filter(
|
||||
generatedcertificate__course_id=course_id,
|
||||
generatedcertificate__status__in=statuses_to_regenerate
|
||||
)
|
||||
else:
|
||||
# compute those students whose certificates are already generated
|
||||
students_already_have_certs = User.objects.filter(
|
||||
~Q(generatedcertificate__status=CertificateStatuses.unavailable),
|
||||
generatedcertificate__course_id=course_id)
|
||||
|
||||
# Return all the enrolled student skipping the ones whose certificates have already been generated
|
||||
return list(set(enrolled_students) - set(students_already_have_certs))
|
||||
|
||||
@@ -22,6 +22,7 @@ from instructor_task.api import (
|
||||
submit_executive_summary_report,
|
||||
submit_course_survey_report,
|
||||
generate_certificates_for_all_students,
|
||||
regenerate_certificates
|
||||
)
|
||||
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
@@ -31,6 +32,7 @@ from instructor_task.tests.test_base import (InstructorTaskTestCase,
|
||||
InstructorTaskModuleTestCase,
|
||||
TestReportMixin,
|
||||
TEST_COURSE_KEY)
|
||||
from certificates.models import CertificateStatuses
|
||||
|
||||
|
||||
class InstructorTaskReportTest(InstructorTaskTestCase):
|
||||
@@ -263,3 +265,18 @@ class InstructorTaskCourseSubmitTest(TestReportMixin, InstructorTaskCourseTestCa
|
||||
self.course.id
|
||||
)
|
||||
self._test_resubmission(api_call)
|
||||
|
||||
def test_regenerate_certificates(self):
|
||||
"""
|
||||
Tests certificates regeneration task submission api
|
||||
"""
|
||||
def api_call():
|
||||
"""
|
||||
wrapper method for regenerate_certificates
|
||||
"""
|
||||
return regenerate_certificates(
|
||||
self.create_task_request(self.instructor),
|
||||
self.course.id,
|
||||
[CertificateStatuses.downloadable, CertificateStatuses.generating]
|
||||
)
|
||||
self._test_resubmission(api_call)
|
||||
|
||||
@@ -1635,3 +1635,149 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
},
|
||||
result
|
||||
)
|
||||
|
||||
def test_certificate_regeneration_for_students(self):
|
||||
"""
|
||||
Verify that certificates are regenerated for all eligible students enrolled in a course whose generated
|
||||
certificate statuses lies in the list 'statuses_to_regenerate' given in task_input.
|
||||
"""
|
||||
# create 10 students
|
||||
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
|
||||
for i in xrange(1, 11)]
|
||||
|
||||
# mark 2 students to have certificates generated already
|
||||
for student in students[:2]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# mark 3 students to have certificates generated with status 'error'
|
||||
for student in students[2:5]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.error,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# mark 6th students to have certificates generated with status 'deleted'
|
||||
for student in students[5:6]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.deleted,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# white-list 7 students
|
||||
for student in students[:7]:
|
||||
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
|
||||
|
||||
current_task = Mock()
|
||||
current_task.update_state = Mock()
|
||||
|
||||
# Certificates should be regenerated for students having generated certificates with status
|
||||
# 'downloadable' or 'error' which are total of 5 students in this test case
|
||||
task_input = {'statuses_to_regenerate': [CertificateStatuses.downloadable, CertificateStatuses.error]}
|
||||
|
||||
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
|
||||
mock_current_task.return_value = current_task
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
|
||||
mock_queue.return_value = (0, "Successfully queued")
|
||||
result = generate_students_certificates(
|
||||
None, None, self.course.id, task_input, 'certificates generated'
|
||||
)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
'action_name': 'certificates generated',
|
||||
'total': 10,
|
||||
'attempted': 5,
|
||||
'succeeded': 5,
|
||||
'failed': 0,
|
||||
'skipped': 5
|
||||
},
|
||||
result
|
||||
)
|
||||
|
||||
def test_certificate_regeneration_with_expected_failures(self):
|
||||
"""
|
||||
Verify that certificates are regenerated for all eligible students enrolled in a course whose generated
|
||||
certificate statuses lies in the list 'statuses_to_regenerate' given in task_input.
|
||||
"""
|
||||
# create 10 students
|
||||
students = [self.create_student(username='student_{}'.format(i), email='student_{}@example.com'.format(i))
|
||||
for i in xrange(1, 11)]
|
||||
|
||||
# mark 2 students to have certificates generated already
|
||||
for student in students[:2]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# mark 3 students to have certificates generated with status 'error'
|
||||
for student in students[2:5]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.error,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# mark 6th students to have certificates generated with status 'deleted'
|
||||
for student in students[5:6]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.deleted,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# mark rest of the 4 students with having generated certificates with status 'generating'
|
||||
# These students are not added in white-list and they have not completed grades so certificate generation
|
||||
# for these students should fail other than the one student that has been added to white-list
|
||||
# so from these students 3 failures and 1 success
|
||||
for student in students[6:]:
|
||||
GeneratedCertificateFactory.create(
|
||||
user=student,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.generating,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
# white-list 7 students
|
||||
for student in students[:7]:
|
||||
CertificateWhitelistFactory.create(user=student, course_id=self.course.id, whitelist=True)
|
||||
|
||||
current_task = Mock()
|
||||
current_task.update_state = Mock()
|
||||
|
||||
# Regenerated certificates for students having generated certificates with status
|
||||
# 'deleted' or 'generating'
|
||||
task_input = {'statuses_to_regenerate': [CertificateStatuses.deleted, CertificateStatuses.generating]}
|
||||
|
||||
with patch('instructor_task.tasks_helper._get_current_task') as mock_current_task:
|
||||
mock_current_task.return_value = current_task
|
||||
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_queue:
|
||||
mock_queue.return_value = (0, "Successfully queued")
|
||||
result = generate_students_certificates(
|
||||
None, None, self.course.id, task_input, 'certificates generated'
|
||||
)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
'action_name': 'certificates generated',
|
||||
'total': 10,
|
||||
'attempted': 5,
|
||||
'succeeded': 2,
|
||||
'failed': 3,
|
||||
'skipped': 5
|
||||
},
|
||||
result
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
var edx = edx || {};
|
||||
var onCertificatesReady = null;
|
||||
|
||||
(function($, gettext, _) {
|
||||
'use strict';
|
||||
@@ -6,7 +7,7 @@ var edx = edx || {};
|
||||
edx.instructor_dashboard = edx.instructor_dashboard || {};
|
||||
edx.instructor_dashboard.certificates = {};
|
||||
|
||||
$(function() {
|
||||
onCertificatesReady = function() {
|
||||
/**
|
||||
* Show a confirmation message before letting staff members
|
||||
* enable/disable self-generated certificates for a course.
|
||||
@@ -59,7 +60,52 @@ var edx = edx || {};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Start regenerating certificates for students.
|
||||
*/
|
||||
$section.on('click', '#btn-start-regenerating-certificates', function(event) {
|
||||
if ( !confirm( gettext('Start regenerating certificates for students in this course?') ) ) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var $btn_regenerating_certs = $(this),
|
||||
$certificate_regeneration_status = $('.certificate-regeneration-status'),
|
||||
url = $btn_regenerating_certs.data('endpoint');
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
data: $("#certificate-regenerating-form").serializeArray(),
|
||||
url: url,
|
||||
success: function (data) {
|
||||
$btn_regenerating_certs.attr('disabled','disabled');
|
||||
if(data.success){
|
||||
$certificate_regeneration_status.text(data.message).
|
||||
removeClass('msg-error').addClass('msg-success');
|
||||
}
|
||||
else{
|
||||
$certificate_regeneration_status.text(data.message).
|
||||
removeClass('msg-success').addClass("msg-error");
|
||||
}
|
||||
},
|
||||
error: function(jqXHR) {
|
||||
try{
|
||||
var response = JSON.parse(jqXHR.responseText);
|
||||
$certificate_regeneration_status.text(gettext(response.message)).
|
||||
removeClass('msg-success').addClass("msg-error");
|
||||
}catch(error){
|
||||
$certificate_regeneration_status.
|
||||
text(gettext('Error while regenerating certificates. Please try again.')).
|
||||
removeClass('msg-success').addClass("msg-error");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Call onCertificatesReady on document.ready event
|
||||
$(onCertificatesReady);
|
||||
|
||||
var Certificates = (function() {
|
||||
function Certificates($section) {
|
||||
|
||||
116
lms/static/js/spec/instructor_dashboard/certificates_spec.js
Normal file
116
lms/static/js/spec/instructor_dashboard/certificates_spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/*global define, onCertificatesReady */
|
||||
define([
|
||||
'jquery',
|
||||
'common/js/spec_helpers/ajax_helpers',
|
||||
'js/instructor_dashboard/certificates'
|
||||
],
|
||||
function($, AjaxHelpers) {
|
||||
'use strict';
|
||||
describe("edx.instructor_dashboard.certificates.regenerate_certificates", function() {
|
||||
var $regenerate_certificates_button = null,
|
||||
$certificate_regeneration_status = null,
|
||||
requests = null;
|
||||
var MESSAGES = {
|
||||
success_message: 'Certificate regeneration task has been started. ' +
|
||||
'You can view the status of the generation task in the "Pending Tasks" section.',
|
||||
error_message: 'Please select one or more certificate statuses that require certificate regeneration.',
|
||||
server_error_message: "Error while regenerating certificates. Please try again."
|
||||
};
|
||||
var expected = {
|
||||
error_class: 'msg-error',
|
||||
success_class: 'msg-success',
|
||||
url: 'test/url/',
|
||||
postData : [],
|
||||
selected_statuses: ['downloadable', 'error'],
|
||||
body: 'certificate_statuses=downloadable&certificate_statuses=error'
|
||||
};
|
||||
|
||||
var select_options = function(option_values){
|
||||
$.each(option_values, function(index, element){
|
||||
$("#certificate-statuses option[value=" + element + "]").attr('selected', 'selected');
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
var fixture = '<section id = "certificates"><h2>Regenerate Certificates</h2>' +
|
||||
'<form id="certificate-regenerating-form" method="post" action="' + expected.url + '">' +
|
||||
' <p id="status-multi-select-tip">Select one or more certificate statuses ' +
|
||||
' below using your mouse and ctrl or command key.</p>' +
|
||||
' <select class="multi-select" multiple id="certificate-statuses" ' +
|
||||
' name="certificate_statuses" aria-describedby="status-multi-select-tip">' +
|
||||
' <option value="downloadable">Downloadable (2)</option>' +
|
||||
' <option value="error">Error (2)</option>' +
|
||||
' <option value="generating">Generating (1)</option>' +
|
||||
' </select>' +
|
||||
' <label for="certificate-statuses">' +
|
||||
' Select certificate statuses that need regeneration and click Regenerate ' +
|
||||
' Certificates button.' +
|
||||
' </label>' +
|
||||
' <input type="button" id="btn-start-regenerating-certificates" value="Regenerate Certificates"' +
|
||||
' data-endpoint="' + expected.url + '"/>' +
|
||||
'</form>' +
|
||||
'<div class="message certificate-regeneration-status"></div></section>';
|
||||
|
||||
setFixtures(fixture);
|
||||
onCertificatesReady();
|
||||
$regenerate_certificates_button = $("#btn-start-regenerating-certificates");
|
||||
$certificate_regeneration_status = $(".certificate-regeneration-status");
|
||||
requests = AjaxHelpers.requests(this);
|
||||
});
|
||||
|
||||
it("does not regenerate certificates if user cancels operation in confirm popup", function() {
|
||||
spyOn(window, 'confirm').andReturn(false);
|
||||
$regenerate_certificates_button.click();
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
AjaxHelpers.expectNoRequests(requests);
|
||||
});
|
||||
|
||||
it("sends regenerate certificates request if user accepts operation in confirm popup", function() {
|
||||
spyOn(window, 'confirm').andReturn(true);
|
||||
$regenerate_certificates_button.click();
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
AjaxHelpers.expectRequest(requests, 'POST', expected.url);
|
||||
});
|
||||
|
||||
it("sends regenerate certificates request with selected certificate statuses", function() {
|
||||
spyOn(window, 'confirm').andReturn(true);
|
||||
|
||||
select_options(expected.selected_statuses);
|
||||
|
||||
$regenerate_certificates_button.click();
|
||||
AjaxHelpers.expectRequest(requests, 'POST', expected.url, expected.body);
|
||||
});
|
||||
|
||||
it("displays error message in case of server side error", function() {
|
||||
spyOn(window, 'confirm').andReturn(true);
|
||||
select_options(expected.selected_statuses);
|
||||
|
||||
$regenerate_certificates_button.click();
|
||||
AjaxHelpers.respondWithError(requests, 500, {message: MESSAGES.server_error_message});
|
||||
expect($certificate_regeneration_status).toHaveClass(expected.error_class);
|
||||
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.server_error_message);
|
||||
});
|
||||
|
||||
it("displays error message returned by the server in case of unsuccessful request", function() {
|
||||
spyOn(window, 'confirm').andReturn(true);
|
||||
select_options(expected.selected_statuses);
|
||||
|
||||
$regenerate_certificates_button.click();
|
||||
AjaxHelpers.respondWithError(requests, 400, {message: MESSAGES.error_message});
|
||||
expect($certificate_regeneration_status).toHaveClass(expected.error_class);
|
||||
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.error_message);
|
||||
});
|
||||
|
||||
it("displays success message returned by the server in case of successful request", function() {
|
||||
spyOn(window, 'confirm').andReturn(true);
|
||||
select_options(expected.selected_statuses);
|
||||
|
||||
$regenerate_certificates_button.click();
|
||||
AjaxHelpers.respondWithJson(requests, {message: MESSAGES.success_message, success: true});
|
||||
expect($certificate_regeneration_status).toHaveClass(expected.success_class);
|
||||
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.success_message);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -293,6 +293,10 @@
|
||||
exports: 'coffee/src/instructor_dashboard/student_admin',
|
||||
deps: ['jquery', 'underscore', 'coffee/src/instructor_dashboard/util', 'string_utils']
|
||||
},
|
||||
'js/instructor_dashboard/certificates': {
|
||||
exports: 'js/instructor_dashboard/certificates',
|
||||
deps: ['jquery', 'gettext', 'underscore']
|
||||
},
|
||||
// LMS class loaded explicitly until they are converted to use RequireJS
|
||||
'js/student_account/account': {
|
||||
exports: 'js/student_account/account',
|
||||
@@ -644,6 +648,7 @@
|
||||
'lms/include/js/spec/instructor_dashboard/ecommerce_spec.js',
|
||||
'lms/include/js/spec/instructor_dashboard/student_admin_spec.js',
|
||||
'lms/include/js/spec/instructor_dashboard/certificates_exception_spec.js',
|
||||
'lms/include/js/spec/instructor_dashboard/certificates_spec.js',
|
||||
'lms/include/js/spec/student_account/account_spec.js',
|
||||
'lms/include/js/spec/student_account/access_spec.js',
|
||||
'lms/include/js/spec/student_account/logistration_factory_spec.js',
|
||||
|
||||
@@ -92,6 +92,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.msg-success{
|
||||
border-top: 2px solid $confirm-color;
|
||||
background: tint($confirm-color,95%);
|
||||
color: $confirm-color;
|
||||
}
|
||||
|
||||
.multi-select {
|
||||
min-width: 150px;
|
||||
|
||||
option {
|
||||
padding: ($baseline/5) $baseline ($baseline/10) ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// inline copy
|
||||
.copy-confirm {
|
||||
color: $confirm-color;
|
||||
@@ -2125,12 +2139,6 @@ input[name="subject"] {
|
||||
}
|
||||
|
||||
#certificate-white-list-editor{
|
||||
.msg-success{
|
||||
border-top: 2px solid $confirm-color;
|
||||
background: tint($confirm-color,95%);
|
||||
color: $confirm-color;
|
||||
}
|
||||
|
||||
.certificate-exception-inputs{
|
||||
.student-username-or-email{
|
||||
width: 300px;
|
||||
|
||||
@@ -92,6 +92,25 @@ import json
|
||||
%endif
|
||||
% endif
|
||||
|
||||
<hr>
|
||||
<p class="start-certificate-regeneration">
|
||||
<h2>${_("Regenerate Certificates")}</h2>
|
||||
<form id="certificate-regenerating-form" method="post" action="${section_data['urls']['start_certificate_regeneration']}">
|
||||
<p id='status-multi-select-tip'>${_('Select one or more certificate statuses below using your mouse and ctrl or command key.')}</p>
|
||||
<select class="multi-select" multiple id="certificate-statuses" name="certificate_statuses" aria-describedby="status-multi-select-tip">
|
||||
%for status in section_data['certificate_statuses']:
|
||||
<option value="${status['status']}">${status['status'].title() + " ({})".format(status['count'])}</option>
|
||||
%endfor
|
||||
</select>
|
||||
<label for="certificate-statuses">
|
||||
${_("Select certificate statuses that need regeneration and click Regenerate Certificates button.")}
|
||||
</label>
|
||||
|
||||
<input type="button" id="btn-start-regenerating-certificates" value="${_('Regenerate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_regeneration']}"/>
|
||||
</form>
|
||||
<div class="message certificate-regeneration-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="certificate_exception-container">
|
||||
<hr>
|
||||
<h2> ${_("Certificate Exceptions")} </h2>
|
||||
|
||||
Reference in New Issue
Block a user