diff --git a/cms/djangoapps/contentstore/views/certificates.py b/cms/djangoapps/contentstore/views/certificates.py index 3c13e502bfb..d28c6813823 100644 --- a/cms/djangoapps/contentstore/views/certificates.py +++ b/cms/djangoapps/contentstore/views/certificates.py @@ -45,7 +45,7 @@ from course_modes.models import CourseMode from edxmako.shortcuts import render_to_response from student.auth import has_studio_write_access -from student.roles import GlobalStaff +from student.roles import GlobalStaff, CourseInstructorRole from util.db import MYSQL_MAX_INT, generate_int_id from util.json_request import JsonResponse from xmodule.modulestore import EdxJSONEncoder @@ -426,6 +426,7 @@ def certificates_list_handler(request, course_key_string): 'certificate_web_view_url': certificate_web_view_url, 'is_active': is_active, 'is_global_staff': GlobalStaff().has_user(request.user), + 'is_course_instructor': CourseInstructorRole(course.id).has_user(request.user), 'certificate_activation_handler_url': activation_handler_url }) elif "application/json" in request.META.get('HTTP_ACCEPT'): diff --git a/cms/static/js/certificates/spec/views/certificate_preview_spec.js b/cms/static/js/certificates/spec/views/certificate_preview_spec.js index 741c7c5996a..78db472d45d 100644 --- a/cms/static/js/certificates/spec/views/certificate_preview_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_preview_spec.js @@ -35,7 +35,7 @@ function(_, $, Course, CertificatePreview, TemplateHelpers, ViewHelpers, AjaxHel num: 'course_num', revision: 'course_rev' }); - window.CMS.User = {isGlobalStaff: true}; + window.CMS.User = {isGlobalStaff: true, isCourseInstructor: true}; TemplateHelpers.installTemplate('certificate-web-preview', true); appendSetFixtures(''); diff --git a/cms/templates/certificates.html b/cms/templates/certificates.html index 13792d216b5..9a696e13c2c 100644 --- a/cms/templates/certificates.html +++ b/cms/templates/certificates.html @@ -29,6 +29,7 @@ CMS.User = CMS.User || {}; CMS.URL.UPLOAD_ASSET = '${upload_asset_url | n, js_escaped_string}'; CMS.User.isGlobalStaff = '${is_global_staff | n, js_escaped_string}'=='True' ? true : false; +CMS.User.isCourseInstructor = '${is_course_instructor | n, js_escaped_string}'=='True' ? true : false; diff --git a/lms/djangoapps/certificates/tests/test_webview_views.py b/lms/djangoapps/certificates/tests/test_webview_views.py index fb74f04a74f..f25bc472950 100644 --- a/lms/djangoapps/certificates/tests/test_webview_views.py +++ b/lms/djangoapps/certificates/tests/test_webview_views.py @@ -969,9 +969,11 @@ def test_render_500_view_invalid_certificate_configuration(self): response = self.client.get(test_url + "?preview=honor") self.assertContains(response, "Invalid Certificate Configuration") - # Verify that Exception is raised when certificate is not in the preview mode - with self.assertRaises(Exception): + # Verify that no Exception is raised when certificate is not in the preview mode + try: self.client.get(test_url) + except Exception: # pylint: disable=broad-except + self.fail("No exception should be raised when rendering an invalid certificate without preview") @override_settings(FEATURES=FEATURES_WITH_CERTS_DISABLED) def test_request_certificate_without_passing(self): diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 46d19fe8ec5..8822068f579 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -433,7 +433,7 @@ def _update_organization_context(context, course): partner_short_name = course.display_organization if course.display_organization else course.org organizations = organization_api.get_course_organizations(course_id=course.id) if organizations: - #TODO Need to add support for multiple organizations, Currently we are interested in the first one. + # TODO Need to add support for multiple organizations, Currently we are interested in the first one. organization = organizations[0] partner_long_name = organization.get('name', partner_long_name) partner_short_name = organization.get('short_name', partner_short_name) @@ -465,6 +465,17 @@ def render_preview_certificate(request, course_id): return render_html_view(request, six.text_type(course_id)) +def _update_minimal_context(context): + """ + This is an eduNEXT function to make the regular certificate work out of the box + """ + context['company_privacy_url'] = '' + context['logo_src'] = '' + context['company_tos_url'] = '' + context['company_about_url'] = '' + context['logo_url'] = '' + + def render_cert_by_uuid(request, certificate_uuid): """ This public view generates an HTML representation of the specified certificate @@ -573,6 +584,8 @@ def render_html_view(request, course_id, certificate=None): with translation.override(certificate_language): context = {'user_language': user_language} + _update_minimal_context(context) # load the bare minimum + _update_context_with_basic_info(context, course_id, platform_name, configuration) context['certificate_data'] = active_configuration diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py index e766dedc0b6..e50f821148f 100644 --- a/lms/djangoapps/instructor/permissions.py +++ b/lms/djangoapps/instructor/permissions.py @@ -15,6 +15,11 @@ ENABLE_CERTIFICATE_GENERATION = 'instructor.enable_certificate_generation' GENERATE_CERTIFICATE_EXCEPTIONS = 'instructor.generate_certificate_exceptions' GENERATE_BULK_CERTIFICATE_EXCEPTIONS = 'instructor.generate_bulk_certificate_exceptions' +GENERATE_EXAMPLE_CERTIFICATES = 'instructor.generate_example_certificates' +START_CERTIFICATE_GENERATION = 'instructor.start_certificate_generation' +START_CERTIFICATE_REGENERATION = 'instructor.start_certificate_regeneration' +CERTIFICATE_EXCEPTION_VIEW = 'instructor.certificate_exception_view' +CERTIFICATE_INVALIDATION_VIEW = 'instructor.certificate_invalidation_view' GIVE_STUDENT_EXTENSION = 'instructor.give_student_extension' VIEW_ISSUED_CERTIFICATES = 'instructor.view_issued_certificates' CAN_RESEARCH = 'instructor.research' @@ -36,9 +41,14 @@ perms[EDIT_COURSE_ACCESS] = HasAccessRule('instructor') perms[EDIT_FORUM_ROLES] = HasAccessRule('staff') perms[EDIT_INVOICE_VALIDATION] = HasAccessRule('staff') -perms[ENABLE_CERTIFICATE_GENERATION] = is_staff -perms[GENERATE_CERTIFICATE_EXCEPTIONS] = is_staff -perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = is_staff +perms[ENABLE_CERTIFICATE_GENERATION] = HasAccessRule('instructor') +perms[GENERATE_CERTIFICATE_EXCEPTIONS] = HasAccessRule('instructor') +perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = HasAccessRule('instructor') +perms[GENERATE_EXAMPLE_CERTIFICATES] = HasAccessRule('instructor') +perms[START_CERTIFICATE_GENERATION] = HasAccessRule('instructor') +perms[START_CERTIFICATE_REGENERATION] = HasAccessRule('instructor') +perms[CERTIFICATE_EXCEPTION_VIEW] = HasAccessRule('instructor') +perms[CERTIFICATE_INVALIDATION_VIEW] = HasAccessRule('instructor') perms[GIVE_STUDENT_EXTENSION] = HasAccessRule('staff') perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') | HasRolesRule('data_researcher') # only global staff or those with the data_researcher role can access the data download tab diff --git a/lms/djangoapps/instructor/tests/test_certificates.py b/lms/djangoapps/instructor/tests/test_certificates.py index 0597f1cd12a..cd71f533603 100644 --- a/lms/djangoapps/instructor/tests/test_certificates.py +++ b/lms/djangoapps/instructor/tests/test_certificates.py @@ -69,9 +69,9 @@ def setUp(self): CertificateGenerationConfiguration.objects.create(enabled=True) def test_visible_only_to_global_staff(self): - # Instructors don't see the certificates section + # Instructors see the certificates section self.client.login(username=self.instructor.username, password="test") - self._assert_certificates_visible(False) + self._assert_certificates_visible(True) # Global staff can see the certificates section self.client.login(username=self.global_staff.username, password="test") @@ -238,10 +238,10 @@ def setUp(self): def test_allow_only_global_staff(self, url_name): url = reverse(url_name, kwargs={'course_id': self.course.id}) - # Instructors do not have access + # Instructors have access self.client.login(username=self.instructor.username, password='test') response = self.client.post(url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 302) # Global staff have access self.client.login(username=self.global_staff.username, password='test') @@ -308,7 +308,7 @@ def test_certificate_generation_api_without_global_staff(self): self.client.login(username=self.instructor.username, password='test') response = self.client.post(url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 200) def test_certificate_generation_api_with_global_staff(self): """ diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index b3c7f6877fc..99facdc8f49 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2963,7 +2963,7 @@ def _instructor_dash_url(course_key, section=None): return url -@require_global_staff +@require_course_permission(permissions.GENERATE_EXAMPLE_CERTIFICATES) @require_POST def generate_example_certificates(request, course_id=None): """Start generating a set of example certificates. @@ -3025,7 +3025,7 @@ def mark_student_can_skip_entrance_exam(request, course_id): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(permissions.START_CERTIFICATE_GENERATION) @require_POST @common_exceptions_400 def start_certificate_generation(request, course_id): @@ -3047,7 +3047,7 @@ def start_certificate_generation(request, course_id): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(permissions.START_CERTIFICATE_REGENERATION) @require_POST @common_exceptions_400 def start_certificate_regeneration(request, course_id): @@ -3089,7 +3089,7 @@ def start_certificate_regeneration(request, course_id): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(permissions.CERTIFICATE_EXCEPTION_VIEW) @require_http_methods(['POST', 'DELETE']) def certificate_exception_view(request, course_id): """ @@ -3401,7 +3401,7 @@ def build_row_errors(key, _user, row_count): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_global_staff +@require_course_permission(permissions.CERTIFICATE_INVALIDATION_VIEW) @require_http_methods(['POST', 'DELETE']) def certificate_invalidation_view(request, course_id): """ diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 21d1a20d6d0..ae8ca0c196b 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -201,7 +201,7 @@ def instructor_dashboard_2(request, course_id): # and enable self-generated certificates for a course. # Note: This is hidden for all CCXs certs_enabled = CertificateGenerationConfiguration.current().enabled and not hasattr(course_key, 'ccx') - if certs_enabled and access['admin']: + if certs_enabled and access['instructor']: sections.append(_section_certificates(course)) openassessment_blocks = modulestore().get_items( diff --git a/lms/static/certificates/images/ico-audit.png b/lms/static/certificates/images/ico-audit.png new file mode 100644 index 00000000000..ab210fde122 Binary files /dev/null and b/lms/static/certificates/images/ico-audit.png differ diff --git a/lms/static/certificates/sass/_components.scss b/lms/static/certificates/sass/_components.scss index f87ec557225..d78479ca6d4 100644 --- a/lms/static/certificates/sass/_components.scss +++ b/lms/static/certificates/sass/_components.scss @@ -482,7 +482,6 @@ } // hide the fancy - .accomplishment-signatories .signatory-signature, .accomplishment-type-symbol { display: none; }