565 lines
23 KiB
Python
565 lines
23 KiB
Python
"""Tests for the certificates panel of the instructor dash. """
|
|
import contextlib
|
|
import ddt
|
|
import mock
|
|
import json
|
|
|
|
from nose.plugins.attrib import attr
|
|
from django.core.urlresolvers import reverse
|
|
from django.test.utils import override_settings
|
|
from django.conf import settings
|
|
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.tests.factories import GeneratedCertificateFactory
|
|
from certificates.models import CertificateGenerationConfiguration, CertificateStatuses
|
|
from certificates import api as certs_api
|
|
|
|
|
|
@attr('shard_1')
|
|
@ddt.ddt
|
|
class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
|
|
"""Tests for the certificate panel of the instructor dash. """
|
|
|
|
ERROR_REASON = "An error occurred!"
|
|
DOWNLOAD_URL = "http://www.example.com/abcd123/cert.pdf"
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CertificatesInstructorDashTest, cls).setUpClass()
|
|
cls.course = CourseFactory.create()
|
|
cls.url = reverse(
|
|
'instructor_dashboard',
|
|
kwargs={'course_id': unicode(cls.course.id)}
|
|
)
|
|
|
|
def setUp(self):
|
|
super(CertificatesInstructorDashTest, self).setUp()
|
|
self.global_staff = GlobalStaffFactory()
|
|
self.instructor = InstructorFactory(course_key=self.course.id)
|
|
|
|
# Need to clear the cache for model-based configuration
|
|
cache.clear()
|
|
|
|
# Enable the certificate generation feature
|
|
CertificateGenerationConfiguration.objects.create(enabled=True)
|
|
|
|
def test_visible_only_to_global_staff(self):
|
|
# Instructors don't see the certificates section
|
|
self.client.login(username=self.instructor.username, password="test")
|
|
self._assert_certificates_visible(False)
|
|
|
|
# Global staff can see the certificates section
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
self._assert_certificates_visible(True)
|
|
|
|
def test_visible_only_when_feature_flag_enabled(self):
|
|
# Disable the feature flag
|
|
CertificateGenerationConfiguration.objects.create(enabled=False)
|
|
cache.clear()
|
|
|
|
# Now even global staff can't see the certificates section
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
self._assert_certificates_visible(False)
|
|
|
|
@ddt.data("started", "error", "success")
|
|
def test_show_certificate_status(self, status):
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
with self._certificate_status("honor", status):
|
|
self._assert_certificate_status("honor", status)
|
|
|
|
def test_show_enabled_button(self):
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
|
|
# Initially, no example certs are generated, so
|
|
# the enable button should be disabled
|
|
self._assert_enable_certs_button_is_disabled()
|
|
|
|
with self._certificate_status("honor", "success"):
|
|
# Certs are disabled for the course, so the enable button should be shown
|
|
self._assert_enable_certs_button(True)
|
|
|
|
# Enable certificates for the course
|
|
certs_api.set_cert_generation_enabled(self.course.id, True)
|
|
|
|
# Now the "disable" button should be shown
|
|
self._assert_enable_certs_button(False)
|
|
|
|
def test_can_disable_even_after_failure(self):
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
|
|
with self._certificate_status("honor", "error"):
|
|
# When certs are disabled for a course, then don't allow them
|
|
# to be enabled if certificate generation doesn't complete successfully
|
|
certs_api.set_cert_generation_enabled(self.course.id, False)
|
|
self._assert_enable_certs_button_is_disabled()
|
|
|
|
# However, if certificates are already enabled, allow them
|
|
# to be disabled even if an error has occurred
|
|
certs_api.set_cert_generation_enabled(self.course.id, True)
|
|
self._assert_enable_certs_button(False)
|
|
|
|
@mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
|
def test_show_enabled_button_for_html_certs(self):
|
|
"""
|
|
Tests `Enable Student-Generated Certificates` button is enabled
|
|
and `Generate Example Certificates` button is not available if
|
|
course has Web/HTML certificates view enabled.
|
|
"""
|
|
self.course.cert_html_view_enabled = True
|
|
self.course.save()
|
|
self.store.update_item(self.course, self.global_staff.id) # pylint: disable=no-member
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
response = self.client.get(self.url)
|
|
self.assertContains(response, 'Enable Student-Generated Certificates')
|
|
self.assertContains(response, 'enable-certificates-submit')
|
|
self.assertNotContains(response, 'Generate Example Certificates')
|
|
|
|
@mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
|
def test_buttons_for_html_certs_in_self_paced_course(self):
|
|
"""
|
|
Tests `Enable Student-Generated Certificates` button is enabled
|
|
and `Generate Certificates` button is not available if
|
|
course has Web/HTML certificates view enabled on a self paced course.
|
|
"""
|
|
self.course.cert_html_view_enabled = True
|
|
self.course.save()
|
|
self.store.update_item(self.course, self.global_staff.id) # pylint: disable=no-member
|
|
self.client.login(username=self.global_staff.username, password="test")
|
|
response = self.client.get(self.url)
|
|
self.assertContains(response, 'Enable Student-Generated Certificates')
|
|
self.assertContains(response, 'enable-certificates-submit')
|
|
self.assertNotContains(response, 'Generate Certificates')
|
|
self.assertNotContains(response, 'btn-start-generating-certificates')
|
|
|
|
def _assert_certificates_visible(self, is_visible):
|
|
"""Check that the certificates section is visible on the instructor dash. """
|
|
response = self.client.get(self.url)
|
|
if is_visible:
|
|
self.assertContains(response, "Student-Generated Certificates")
|
|
else:
|
|
self.assertNotContains(response, "Student-Generated Certificates")
|
|
|
|
@contextlib.contextmanager
|
|
def _certificate_status(self, description, status):
|
|
"""Configure the certificate status by mocking the certificates API. """
|
|
patched = 'instructor.views.instructor_dashboard.certs_api.example_certificates_status'
|
|
with mock.patch(patched) as certs_api_status:
|
|
cert_status = [{
|
|
'description': description,
|
|
'status': status
|
|
}]
|
|
|
|
if status == 'error':
|
|
cert_status[0]['error_reason'] = self.ERROR_REASON
|
|
if status == 'success':
|
|
cert_status[0]['download_url'] = self.DOWNLOAD_URL
|
|
|
|
certs_api_status.return_value = cert_status
|
|
yield
|
|
|
|
def _assert_certificate_status(self, cert_name, expected_status):
|
|
"""Check the certificate status display on the instructor dash. """
|
|
response = self.client.get(self.url)
|
|
|
|
if expected_status == 'started':
|
|
expected = 'Generating example {name} certificate'.format(name=cert_name)
|
|
self.assertContains(response, expected)
|
|
elif expected_status == 'error':
|
|
expected = self.ERROR_REASON
|
|
self.assertContains(response, expected)
|
|
elif expected_status == 'success':
|
|
expected = self.DOWNLOAD_URL
|
|
self.assertContains(response, expected)
|
|
else:
|
|
self.fail("Invalid certificate status: {status}".format(status=expected_status))
|
|
|
|
def _assert_enable_certs_button_is_disabled(self):
|
|
"""Check that the "enable student-generated certificates" button is disabled. """
|
|
response = self.client.get(self.url)
|
|
expected_html = '<button class="is-disabled" disabled>Enable Student-Generated Certificates</button>'
|
|
self.assertContains(response, expected_html)
|
|
|
|
def _assert_enable_certs_button(self, is_enabled):
|
|
"""Check whether the button says "enable" or "disable" cert generation. """
|
|
response = self.client.get(self.url)
|
|
expected_html = (
|
|
'Enable Student-Generated Certificates' if is_enabled
|
|
else 'Disable Student-Generated Certificates'
|
|
)
|
|
self.assertContains(response, expected_html)
|
|
|
|
|
|
@attr('shard_1')
|
|
@override_settings(CERT_QUEUE='certificates')
|
|
@ddt.ddt
|
|
class CertificatesInstructorApiTest(SharedModuleStoreTestCase):
|
|
"""Tests for the certificates end-points in the instructor dash API. """
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(CertificatesInstructorApiTest, cls).setUpClass()
|
|
cls.course = CourseFactory.create()
|
|
|
|
def setUp(self):
|
|
super(CertificatesInstructorApiTest, self).setUp()
|
|
self.global_staff = GlobalStaffFactory()
|
|
self.instructor = InstructorFactory(course_key=self.course.id)
|
|
self.user = UserFactory()
|
|
|
|
# Enable certificate generation
|
|
self.certificate_exception_data = [
|
|
dict(
|
|
created="Wednesday, October 28, 2015",
|
|
notes="Test Notes for Test Certificate Exception",
|
|
user_email='',
|
|
user_id='',
|
|
user_name=unicode(self.user.username)
|
|
),
|
|
]
|
|
|
|
cache.clear()
|
|
CertificateGenerationConfiguration.objects.create(enabled=True)
|
|
|
|
@ddt.data('generate_example_certificates', 'enable_certificate_generation')
|
|
def test_allow_only_global_staff(self, url_name):
|
|
url = reverse(url_name, kwargs={'course_id': self.course.id})
|
|
|
|
# Instructors do not have access
|
|
self.client.login(username=self.instructor.username, password='test')
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
# Global staff have access
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_generate_example_certificates(self):
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'generate_example_certificates',
|
|
kwargs={'course_id': unicode(self.course.id)}
|
|
)
|
|
response = self.client.post(url)
|
|
|
|
# Expect a redirect back to the instructor dashboard
|
|
self._assert_redirects_to_instructor_dash(response)
|
|
|
|
# Expect that certificate generation started
|
|
# Cert generation will fail here because XQueue isn't configured,
|
|
# but the status should at least not be None.
|
|
status = certs_api.example_certificates_status(self.course.id)
|
|
self.assertIsNot(status, None)
|
|
|
|
@ddt.data(True, False)
|
|
def test_enable_certificate_generation(self, is_enabled):
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'enable_certificate_generation',
|
|
kwargs={'course_id': unicode(self.course.id)}
|
|
)
|
|
params = {'certificates-enabled': 'true' if is_enabled else 'false'}
|
|
response = self.client.post(url, data=params)
|
|
|
|
# Expect a redirect back to the instructor dashboard
|
|
self._assert_redirects_to_instructor_dash(response)
|
|
|
|
# Expect that certificate generation is now enabled for the course
|
|
actual_enabled = certs_api.cert_generation_enabled(self.course.id)
|
|
self.assertEqual(is_enabled, actual_enabled)
|
|
|
|
def _assert_redirects_to_instructor_dash(self, response):
|
|
"""Check that the response redirects to the certificates section. """
|
|
expected_redirect = reverse(
|
|
'instructor_dashboard',
|
|
kwargs={'course_id': unicode(self.course.id)}
|
|
)
|
|
expected_redirect += '#view-certificates'
|
|
self.assertRedirects(response, expected_redirect)
|
|
|
|
def test_certificate_generation_api_without_global_staff(self):
|
|
"""
|
|
Test certificates generation api endpoint returns permission denied if
|
|
user who made the request is not member of global staff.
|
|
"""
|
|
user = UserFactory.create()
|
|
self.client.login(username=user.username, password='test')
|
|
url = reverse(
|
|
'start_certificate_generation',
|
|
kwargs={'course_id': unicode(self.course.id)}
|
|
)
|
|
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
self.client.login(username=self.instructor.username, password='test')
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
def test_certificate_generation_api_with_global_staff(self):
|
|
"""
|
|
Test certificates generation api endpoint returns success status when called with
|
|
valid course key
|
|
"""
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'start_certificate_generation',
|
|
kwargs={'course_id': unicode(self.course.id)}
|
|
)
|
|
|
|
response = self.client.post(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
res_json = json.loads(response.content)
|
|
self.assertIsNotNone(res_json['message'])
|
|
self.assertIsNotNone(res_json['task_id'])
|
|
|
|
def test_certificate_exception_added_successfully(self):
|
|
"""
|
|
Test certificates exception addition api endpoint returns success status and updated certificate exception data
|
|
when called with valid course key and certificate exception data
|
|
"""
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
|
)
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json'
|
|
)
|
|
|
|
# Assert successful request processing
|
|
self.assertEqual(response.status_code, 200)
|
|
res_json = json.loads(response.content)
|
|
|
|
# Assert Request was successful
|
|
self.assertTrue(res_json['success'])
|
|
|
|
# Assert Success Message
|
|
self.assertEqual(res_json['message'], u'Students added to Certificate white list successfully')
|
|
|
|
# Assert Certificate Exception Updated data
|
|
certificate_exception = json.loads(res_json['data'])[0]
|
|
self.assertEqual(certificate_exception['user_email'], self.user.email)
|
|
self.assertEqual(certificate_exception['user_name'], self.user.username)
|
|
self.assertEqual(certificate_exception['user_id'], self.user.id) # pylint: disable=no-member
|
|
|
|
def test_certificate_exception_invalid_username_error(self):
|
|
"""
|
|
Test certificates exception addition api endpoint returns failure when called with
|
|
invalid username.
|
|
"""
|
|
invalid_user = 'test_invalid_user_name'
|
|
self.certificate_exception_data[0].update({'user_name': invalid_user})
|
|
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
|
)
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json')
|
|
|
|
# Assert 400 status code in response
|
|
self.assertEqual(response.status_code, 400)
|
|
res_json = json.loads(response.content)
|
|
|
|
# Assert Request not successful
|
|
self.assertFalse(res_json['success'])
|
|
|
|
# Assert Error Message
|
|
self.assertEqual(
|
|
res_json['message'],
|
|
u'Student (username/email={user}) does not exist'.format(user=invalid_user)
|
|
)
|
|
|
|
def test_certificate_exception_missing_username_and_email_error(self):
|
|
"""
|
|
Test certificates exception addition api endpoint returns failure when called with
|
|
missing username/email.
|
|
"""
|
|
self.certificate_exception_data[0].update({'user_name': '', 'user_email': ''})
|
|
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
|
)
|
|
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json')
|
|
|
|
# Assert 400 status code in response
|
|
self.assertEqual(response.status_code, 400)
|
|
res_json = json.loads(response.content)
|
|
|
|
# Assert Request not successful
|
|
self.assertFalse(res_json['success'])
|
|
|
|
# Assert Error Message
|
|
self.assertEqual(
|
|
res_json['message'],
|
|
u'Student username/email is required.'
|
|
)
|
|
|
|
def test_certificate_exception_duplicate_user_error(self):
|
|
"""
|
|
Test certificates exception addition api endpoint returns failure when called with
|
|
username/email that already exists in 'CertificateWhitelist' table.
|
|
"""
|
|
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
url = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
|
)
|
|
|
|
self.client.post(
|
|
url,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json'
|
|
)
|
|
|
|
# Make some request again to simulate duplicate user scenario
|
|
response = self.client.post(
|
|
url,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json'
|
|
)
|
|
|
|
# Assert 400 status code in response
|
|
self.assertEqual(response.status_code, 400)
|
|
res_json = json.loads(response.content)
|
|
|
|
# Assert Request not successful
|
|
self.assertFalse(res_json['success'])
|
|
|
|
user = self.certificate_exception_data[0]['user_name']
|
|
# Assert Error Message
|
|
self.assertEqual(
|
|
res_json['message'],
|
|
u"Student (username/email={user_id} already in certificate exception list)".format(user_id=user)
|
|
)
|
|
|
|
def test_certificate_exception_same_user_in_two_different_courses(self):
|
|
"""
|
|
Test certificates exception addition api endpoint in scenario when same
|
|
student is added to two different courses.
|
|
"""
|
|
|
|
self.client.login(username=self.global_staff.username, password='test')
|
|
|
|
url_course1 = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(self.course.id), 'white_list_student': ''}
|
|
)
|
|
|
|
response = self.client.post(
|
|
url_course1,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json'
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
res_json = json.loads(response.content)
|
|
self.assertTrue(res_json['success'])
|
|
|
|
course2 = CourseFactory.create()
|
|
url_course2 = reverse(
|
|
'create_certificate_exception',
|
|
kwargs={'course_id': unicode(course2.id), 'white_list_student': ''}
|
|
)
|
|
|
|
# add certificate exception for same user in a different course
|
|
self.client.post(
|
|
url_course2,
|
|
data=json.dumps(self.certificate_exception_data),
|
|
content_type='application/json'
|
|
)
|
|
|
|
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.')
|