Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/views/certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.auth import has_studio_write_access
from common.djangoapps.student.roles import GlobalStaff
from common.djangoapps.student.roles import GlobalStaff, CourseInstructorRole
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
from common.djangoapps.util.json_request import JsonResponse
from xmodule.modulestore import EdxJSONEncoder # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -427,6 +427,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,
'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course.id),
})
Expand Down
14 changes: 11 additions & 3 deletions lms/djangoapps/instructor/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
ENABLE_CERTIFICATE_GENERATION = 'instructor.enable_certificate_generation'
GENERATE_CERTIFICATE_EXCEPTIONS = 'instructor.generate_certificate_exceptions'
GENERATE_BULK_CERTIFICATE_EXCEPTIONS = 'instructor.generate_bulk_certificate_exceptions'
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'
Expand All @@ -38,9 +42,13 @@
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] = is_staff | HasAccessRule('instructor')
perms[GENERATE_CERTIFICATE_EXCEPTIONS] = is_staff | HasAccessRule('instructor')
perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = is_staff | HasAccessRule('instructor')
perms[START_CERTIFICATE_GENERATION] = is_staff | HasAccessRule('instructor')
perms[START_CERTIFICATE_REGENERATION] = is_staff | HasAccessRule('instructor')
perms[CERTIFICATE_EXCEPTION_VIEW] = is_staff | HasAccessRule('instructor')
perms[CERTIFICATE_INVALIDATION_VIEW] = is_staff | 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
Expand Down
7 changes: 7 additions & 0 deletions lms/djangoapps/instructor/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ def plugin_settings(settings):
# .. toggle_use_cases: opt_in
'CERTIFICATES_INSTRUCTOR_GENERATION': False,

# .. toggle_name: FEATURES['ENABLE_CERTIFICATES_INSTRUCTOR_MANAGE] # lint-amnesty, pylint: disable=annotation-missing-token
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Allow course instructors to manage certificates from the instructor dashboard.
# .. toggle_use_cases: opt_in
'ENABLE_CERTIFICATES_INSTRUCTOR_MANAGE': False,

# .. toggle_name: FEATURES['BATCH_ENROLLMENT_NOTIFY_USERS_DEFAULT']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: True
Expand Down
23 changes: 14 additions & 9 deletions lms/djangoapps/instructor/tests/test_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,16 @@ def test_visible_only_when_feature_flag_enabled(self):
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
self._assert_certificates_visible(False)

@mock.patch.dict(settings.FEATURES, {'ENABLE_CERTIFICATES_INSTRUCTOR_MANAGE': True})
def test_visible_for_instructors_when_feature_is_enabled(self):
self.client.login(username=self.instructor.username, password=self.TEST_PASSWORD)
self._assert_certificates_visible(True)

@ddt.data("started", "error", "success")
def test_show_certificate_status(self, status):
def test_show_certificate_status(self, certificate_status):
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
with self._certificate_status("honor", status):
self._assert_certificate_status("honor", status)
with self._certificate_status("honor", certificate_status):
self._assert_certificate_status("honor", certificate_status)

def test_show_enabled_button(self):
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
Expand Down Expand Up @@ -159,18 +164,18 @@ def _assert_certificates_visible(self, is_visible):
self.assertNotContains(response, "Student-Generated Certificates")

@contextlib.contextmanager
def _certificate_status(self, description, status):
def _certificate_status(self, description, certificate_status):
"""Configure the certificate status by mocking the certificates API. """
patched = 'lms.djangoapps.instructor.views.instructor_dashboard.certs_api.example_certificates_status'
with mock.patch(patched) as certs_api_status:
cert_status = [{
'description': description,
'status': status
'status': certificate_status
}]

if status == 'error':
if certificate_status == 'error':
cert_status[0]['error_reason'] = self.ERROR_REASON
if status == 'success':
if certificate_status == 'success':
cert_status[0]['download_url'] = self.DOWNLOAD_URL

certs_api_status.return_value = cert_status
Expand Down Expand Up @@ -235,7 +240,7 @@ def test_allow_only_global_staff(self, url_name):
# Instructors do not have access
self.client.login(username=self.instructor.username, password=self.TEST_PASSWORD)
response = self.client.post(url)
assert response.status_code == 403
assert response.status_code == 302

# Global staff have access
self.client.login(username=self.global_staff.username, password=self.TEST_PASSWORD)
Expand Down Expand Up @@ -285,7 +290,7 @@ def test_certificate_generation_api_without_global_staff(self):

self.client.login(username=self.instructor.username, password=self.TEST_PASSWORD)
response = self.client.post(url)
assert response.status_code == 403
assert response.status_code == 200

def test_certificate_generation_api_with_global_staff(self):
"""
Expand Down
10 changes: 5 additions & 5 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
store_uploaded_file,
)
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest
from common.djangoapps.util.views import require_global_staff
from common.djangoapps.util.views import require_global_staff # pylint: disable=unused-import
from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled, create_course_email
from lms.djangoapps.certificates import api as certs_api
from lms.djangoapps.certificates.models import (
Expand Down Expand Up @@ -3019,7 +3019,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):
Expand All @@ -3041,7 +3041,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):
Expand Down Expand Up @@ -3083,7 +3083,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):
"""
Expand Down Expand Up @@ -3394,7 +3394,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):
"""
Expand Down
4 changes: 3 additions & 1 deletion lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
# 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']:
certs_instructor_enabled = settings.FEATURES.get('ENABLE_CERTIFICATES_INSTRUCTOR_MANAGE', False)

if certs_enabled and (access['admin'] or (access['instructor'] and certs_instructor_enabled)):
sections.append(_section_certificates(course))

openassessment_blocks = modulestore().get_items(
Expand Down