diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py index 879d620bba..bbe702d81b 100644 --- a/common/djangoapps/student/roles.py +++ b/common/djangoapps/student/roles.py @@ -343,6 +343,14 @@ class OrgLibraryUserRole(OrgRole): super(OrgLibraryUserRole, self).__init__(self.ROLE, *args, **kwargs) +class OrgDataResearcherRole(OrgRole): + """A Data Researcher""" + ROLE = 'data_researcher' + + def __init__(self, *args, **kwargs): + super(OrgDataResearcherRole, self).__init__(self.ROLE, *args, **kwargs) + + @register_access_role class CourseCreatorRole(RoleBase): """ diff --git a/common/djangoapps/student/rules.py b/common/djangoapps/student/rules.py deleted file mode 100644 index c718d56af4..0000000000 --- a/common/djangoapps/student/rules.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Django rules for student roles -""" -from __future__ import absolute_import - -import rules - -from lms.djangoapps.courseware.access import has_access -from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlag, WaffleFlagNamespace - -from .roles import CourseDataResearcherRole - -# Waffle flag to enable the separate course outline page and full width content. -RESEARCHER_ROLE = CourseWaffleFlag(WaffleFlagNamespace(name='instructor'), 'researcher') - - -@rules.predicate -def can_access_reports(user, course_id): - """ - Returns whether the user can access the course data downloads. - """ - is_staff = user.is_staff - if RESEARCHER_ROLE.is_enabled(course_id): - return is_staff or CourseDataResearcherRole(course_id).has_user(user) - else: - return is_staff or has_access(user, 'staff', course_id) - -rules.add_perm('student.can_research', can_access_reports) diff --git a/lms/djangoapps/courseware/rules.py b/lms/djangoapps/courseware/rules.py index 72a4ea903f..58866b397a 100644 --- a/lms/djangoapps/courseware/rules.py +++ b/lms/djangoapps/courseware/rules.py @@ -19,10 +19,12 @@ from xblock.core import XBlock from course_modes.models import CourseMode from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from student.models import CourseAccessRole, CourseEnrollment +from student.roles import CourseRole, OrgRole from xmodule.course_module import CourseDescriptor from xmodule.error_module import ErrorDescriptor from xmodule.x_module import XModule + from .access import has_access LOG = logging.getLogger(__name__) @@ -151,3 +153,27 @@ class HasStaffAccessToContent(Rule): if not is_global_staff: query &= Q(id__in=course_staff_or_instructor_courses) | Q(org__in=org_staff_or_instructor_courses) return query + + +class HasRolesRule(Rule): + def __init__(self, *roles): + self.roles = roles + + def check(self, user, instance=None): + if not user.is_authenticated: + return False + if isinstance(instance, CourseKey): + course_key = instance + elif isinstance(instance, (CourseDescriptor, CourseOverview)): + course_key = instance.id + elif isinstance(instance, (ErrorDescriptor, XModule, XBlock)): + course_key = instance.scope_ids.usage_id.course_key + else: + course_key = CourseKey.from_string(str(instance)) + + for role in self.roles: + if CourseRole(role, course_key).has_user(user): + return True + if OrgRole(role, course_key.org).has_user(user): + return True + return False diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py index e1d75a235b..8a94e492aa 100644 --- a/lms/djangoapps/instructor/permissions.py +++ b/lms/djangoapps/instructor/permissions.py @@ -4,7 +4,8 @@ Permissions for the instructor dashboard and associated actions from bridgekeeper import perms from bridgekeeper.rules import is_staff -from lms.djangoapps.courseware.rules import HasAccessRule +from lms.djangoapps.courseware.rules import HasAccessRule, HasRolesRule + ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM = 'instructor.allow_student_to_bypass_entrance_exam' ASSIGN_TO_COHORTS = 'instructor.assign_to_cohorts' @@ -16,6 +17,18 @@ GENERATE_CERTIFICATE_EXCEPTIONS = 'instructor.generate_certificate_exceptions' GENERATE_BULK_CERTIFICATE_EXCEPTIONS = 'instructor.generate_bulk_certificate_exceptions' GIVE_STUDENT_EXTENSION = 'instructor.give_student_extension' VIEW_ISSUED_CERTIFICATES = 'instructor.view_issued_certificates' +CAN_RESEARCH = 'instructor.research' +CAN_ENROLL = 'instructor.enroll' +CAN_BETATEST = 'instructor.enroll_beta' +ENROLLMENT_REPORT = 'instructor.enrollment_report' +EXAM_RESULTS = 'instructor.view_exam_results' +OVERRIDE_GRADES = 'instructor.override_grades' +SHOW_TASKS = 'instructor.show_tasks' +VIEW_COUPONS = 'instructor.view_coupons' +EMAIL = 'instructor.email' +RESCORE_EXAMS = 'instructor.rescore_exams' +VIEW_REGISTRATION = 'instructor.view_registration' +VIEW_DASHBOARD = 'instructor.dashboard' perms[ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM] = HasAccessRule('staff') @@ -27,4 +40,21 @@ perms[ENABLE_CERTIFICATE_GENERATION] = is_staff perms[GENERATE_CERTIFICATE_EXCEPTIONS] = is_staff perms[GENERATE_BULK_CERTIFICATE_EXCEPTIONS] = is_staff perms[GIVE_STUDENT_EXTENSION] = HasAccessRule('staff') -perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') +perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') | HasRolesRule('data_researcher') +perms[CAN_RESEARCH] = HasRolesRule('data_researcher') +perms[CAN_ENROLL] = HasAccessRule('staff') +perms[CAN_BETATEST] = HasAccessRule('instructor') +perms[ENROLLMENT_REPORT] = HasAccessRule('staff') | HasRolesRule('data_researcher') +perms[VIEW_COUPONS] = HasAccessRule('staff') | HasRolesRule('data_researcher') +perms[EXAM_RESULTS] = HasAccessRule('staff') +perms[OVERRIDE_GRADES] = HasAccessRule('staff') +perms[SHOW_TASKS] = HasAccessRule('staff') | HasRolesRule('data_researcher') +perms[EMAIL] = HasAccessRule('staff') +perms[RESCORE_EXAMS] = HasAccessRule('instructor') +perms[VIEW_REGISTRATION] = HasAccessRule('staff') +perms[VIEW_DASHBOARD] = \ + HasRolesRule( + 'staff', + 'instructor', + 'data_researcher' +) | HasAccessRule('staff') | HasAccessRule('instructor') diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 3bbfdbf54f..856d5ac5fb 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -31,7 +31,7 @@ from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from shoppingcart.models import CourseRegCodeItem, Order, PaidCourseRegistration from student.models import CourseEnrollment from student.roles import CourseFinanceAdminRole -from student.tests.factories import AdminFactory, CourseEnrollmentFactory +from student.tests.factories import AdminFactory, CourseAccessRoleFactory, CourseEnrollmentFactory from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls @@ -118,6 +118,24 @@ class TestInstructorDashboard(ModuleStoreTestCase, LoginEnrollmentTestCase, XssT student = UserFactory.create() self.assertFalse(has_instructor_tab(student, self.course)) + researcher = UserFactory.create() + CourseAccessRoleFactory( + course_id=self.course.id, + user=researcher, + role='data_researcher', + org=self.course.id.org + ) + self.assertTrue(has_instructor_tab(researcher, self.course)) + + org_researcher = UserFactory.create() + CourseAccessRoleFactory( + course_id=None, + user=org_researcher, + role='data_researcher', + org=self.course.id.org + ) + self.assertTrue(has_instructor_tab(org_researcher, self.course)) + @ddt.data( ("How to defeat the Road Runner", "2017", "001", "ACME"), ) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index ca3a1f3d38..b85c901c7f 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -37,7 +37,8 @@ from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthenticat from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey -from rest_framework import permissions, status +from rest_framework import status +from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response from rest_framework.views import APIView from six import StringIO, text_type @@ -135,18 +136,8 @@ from util.json_request import JsonResponse, JsonResponseBadRequest from util.views import require_global_staff from xmodule.modulestore.django import modulestore -from ..permissions import ( - ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM, - ASSIGN_TO_COHORTS, - EDIT_COURSE_ACCESS, - EDIT_FORUM_ROLES, - EDIT_INVOICE_VALIDATION, - ENABLE_CERTIFICATE_GENERATION, - GENERATE_BULK_CERTIFICATE_EXCEPTIONS, - GENERATE_CERTIFICATE_EXCEPTIONS, - GIVE_STUDENT_EXTENSION, - VIEW_ISSUED_CERTIFICATES -) +from .. import permissions + from .tools import ( dump_module_extensions, dump_student_extensions, @@ -231,36 +222,6 @@ def require_post_params(*args, **kwargs): return decorator -def require_level(level, perm=None): - """ - Decorator with argument that requires an access level of the requesting - user. If the requirement is not satisfied, returns an - HttpResponseForbidden (403). - - Assumes that request is in args[0]. - Assumes that course_id is in kwargs['course_id']. - - `level` is in ['instructor', 'staff'] - if `level` is 'staff', instructors will also be allowed, even - if they are not in the staff group. - """ - if level not in ['instructor', 'staff']: - raise ValueError(u"unrecognized level '{}'".format(level)) - - def decorator(func): # pylint: disable=missing-docstring - def wrapped(*args, **kwargs): - request = args[0] - course = get_course_by_id(CourseKey.from_string(kwargs['course_id'])) - - if has_access(request.user, level, course) and \ - ((perm and request.user.has_perm(perm, course.id)) or not perm): - return func(*args, **kwargs) - else: - return HttpResponseForbidden() - return wrapped - return decorator - - def require_course_permission(permission): """ Decorator with argument that requires a specific permission of the requesting @@ -342,7 +303,7 @@ COUNTRY_INDEX = 3 @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_ENROLL) def register_and_enroll_students(request, course_id): # pylint: disable=too-many-statements """ Create new account and Enroll students in this course. @@ -658,7 +619,7 @@ def create_and_enroll_user(email, username, name, country, password, course_id, @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_ENROLL) @require_post_params(action="enroll or unenroll", identifiers="stringified list of emails and/or usernames") def students_update_enrollment(request, course_id): """ @@ -826,7 +787,7 @@ def students_update_enrollment(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('instructor') +@require_course_permission(permissions.CAN_BETATEST) @common_exceptions_400 @require_post_params( identifiers="stringified list of emails and/or usernames", @@ -910,7 +871,7 @@ def bulk_beta_modify_access(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(EDIT_COURSE_ACCESS) +@require_course_permission(permissions.EDIT_COURSE_ACCESS) @require_post_params( unique_student_identifier="email or username of user to change access", rolename="'instructor', 'staff', 'beta', or 'ccx_coach'", @@ -991,7 +952,7 @@ def modify_access(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('instructor') +@require_course_permission(permissions.EDIT_COURSE_ACCESS) @require_post_params(rolename="'instructor', 'staff', or 'beta'") def list_course_role_members(request, course_id): """ @@ -1045,7 +1006,7 @@ def list_course_role_members(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def get_problem_responses(request, course_id): """ @@ -1085,15 +1046,17 @@ def get_problem_responses(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_RESEARCH) def get_grading_config(request, course_id): """ Respond with json which contains a html formatted grade summary. """ course_id = CourseKey.from_string(course_id) - course = get_course_with_access( - request.user, 'staff', course_id, depth=None - ) + # course = get_course_with_access( + # request.user, 'staff', course_id, depth=None + # ) + course = get_course_by_id(course_id) + grading_config_summary = instructor_analytics.basic.dump_grading_context(course) response_payload = { @@ -1105,7 +1068,7 @@ def get_grading_config(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) def get_sale_records(request, course_id, csv=False): # pylint: disable=unused-argument, redefined-outer-name """ return the summary of all sales records for a particular course @@ -1136,7 +1099,7 @@ def get_sale_records(request, course_id, csv=False): # pylint: disable=unused-a @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) def get_sale_order_records(request, course_id): # pylint: disable=unused-argument """ return the summary of all sales records for a particular course @@ -1176,7 +1139,7 @@ def get_sale_order_records(request, course_id): # pylint: disable=unused-argume return instructor_analytics.csvs.create_csv_response("e-commerce_sale_order_records.csv", csv_columns, datarows) -@require_course_permission(EDIT_INVOICE_VALIDATION) +@require_course_permission(permissions.EDIT_INVOICE_VALIDATION) @require_POST def sale_validation(request, course_id): """ @@ -1243,7 +1206,7 @@ def re_validate_invoice(obj_invoice): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(VIEW_ISSUED_CERTIFICATES) +@require_course_permission(permissions.VIEW_ISSUED_CERTIFICATES) def get_issued_certificates(request, course_id): """ Responds with JSON if CSV is not required. contains a list of issued certificates. @@ -1284,7 +1247,7 @@ def get_issued_certificates(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def get_students_features(request, course_id, csv=False): # pylint: disable=redefined-outer-name """ @@ -1381,7 +1344,7 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def get_students_who_may_enroll(request, course_id): """ @@ -1428,7 +1391,7 @@ def _cohorts_csv_validator(file_storage, file_to_validate): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @require_POST -@require_course_permission(ASSIGN_TO_COHORTS) +@require_course_permission(permissions.ASSIGN_TO_COHORTS) @common_exceptions_400 def add_users_to_cohorts(request, course_id): """ @@ -1474,7 +1437,7 @@ class CohortCSV(DeveloperErrorViewMixin, APIView): BearerAuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser, ) - permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser) + permission_classes = (IsAuthenticated, IsAdminUser) def post(self, request, course_key_string): """ @@ -1498,7 +1461,7 @@ class CohortCSV(DeveloperErrorViewMixin, APIView): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_COUPONS) def get_coupon_codes(request, course_id): # pylint: disable=unused-argument """ Respond with csv which contains a summary of all Active Coupons. @@ -1529,7 +1492,7 @@ def get_coupon_codes(request, course_id): # pylint: disable=unused-argument @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.ENROLLMENT_REPORT) @require_finance_admin @common_exceptions_400 def get_enrollment_report(request, course_id): @@ -1548,7 +1511,7 @@ def get_enrollment_report(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.ENROLLMENT_REPORT) @require_finance_admin @common_exceptions_400 def get_exec_summary_report(request, course_id): @@ -1567,7 +1530,7 @@ def get_exec_summary_report(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.ENROLLMENT_REPORT) @common_exceptions_400 def get_course_survey_results(request, course_id): """ @@ -1585,7 +1548,7 @@ def get_course_survey_results(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.EXAM_RESULTS) @common_exceptions_400 def get_proctored_exam_results(request, course_id): """ @@ -1674,7 +1637,7 @@ def random_code_generator(): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_COUPONS) @require_POST def get_registration_codes(request, course_id): """ @@ -1869,7 +1832,7 @@ def generate_registration_codes(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_COUPONS) @require_POST def active_registration_codes(request, course_id): """ @@ -1900,7 +1863,7 @@ def active_registration_codes(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_COUPONS) @require_POST def spent_registration_codes(request, course_id): """ @@ -1931,7 +1894,7 @@ def spent_registration_codes(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) def get_anon_ids(request, course_id): # pylint: disable=unused-argument """ Respond with 2-column CSV output of user-id, anonymized-user-id @@ -1969,7 +1932,7 @@ def get_anon_ids(request, course_id): # pylint: disable=unused-argument @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_ENROLL) @require_post_params( unique_student_identifier="email or username of student for whom to get enrollment status" ) @@ -2026,7 +1989,7 @@ def get_student_enrollment_status(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) @common_exceptions_400 -@require_level('staff') +@require_course_permission(permissions.ENROLLMENT_REPORT) @require_post_params( unique_student_identifier="email or username of student for whom to get progress url" ) @@ -2057,7 +2020,7 @@ def get_student_progress_url(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @require_post_params( problem_to_reset="problem urlname to reset" ) @@ -2144,7 +2107,7 @@ def reset_student_attempts(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @common_exceptions_400 def reset_student_attempts_for_entrance_exam(request, course_id): """ @@ -2219,7 +2182,7 @@ def reset_student_attempts_for_entrance_exam(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.OVERRIDE_GRADES) @require_post_params(problem_to_reset="problem urlname to reset") @common_exceptions_400 def rescore_problem(request, course_id): @@ -2295,7 +2258,7 @@ def rescore_problem(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.OVERRIDE_GRADES) @require_post_params(problem_to_reset="problem urlname to reset", score='overriding score') @common_exceptions_400 def override_problem_score(request, course_id): @@ -2352,7 +2315,7 @@ def override_problem_score(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('instructor') +@require_course_permission(permissions.RESCORE_EXAMS) @common_exceptions_400 def rescore_entrance_exam(request, course_id): """ @@ -2409,7 +2372,7 @@ def rescore_entrance_exam(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.EMAIL) def list_background_email_tasks(request, course_id): # pylint: disable=unused-argument """ List background email tasks. @@ -2431,7 +2394,7 @@ def list_background_email_tasks(request, course_id): # pylint: disable=unused-a @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.EMAIL) def list_email_content(request, course_id): # pylint: disable=unused-argument """ List the content of bulk emails sent @@ -2450,7 +2413,7 @@ def list_email_content(request, course_id): # pylint: disable=unused-argument @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.SHOW_TASKS) def list_instructor_tasks(request, course_id): """ List instructor tasks. @@ -2496,7 +2459,7 @@ def list_instructor_tasks(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.SHOW_TASKS) def list_entrance_exam_instructor_tasks(request, course_id): """ List entrance exam related instructor tasks. @@ -2538,7 +2501,7 @@ def list_entrance_exam_instructor_tasks(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_RESEARCH) def list_report_downloads(request, course_id): """ List grade CSV files that are available for download for this course. @@ -2562,7 +2525,7 @@ def list_report_downloads(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @require_finance_admin def list_financial_report_downloads(_request, course_id): """ @@ -2584,7 +2547,7 @@ def list_financial_report_downloads(_request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def export_ora2_data(request, course_id): """ @@ -2602,7 +2565,7 @@ def export_ora2_data(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def calculate_grades_csv(request, course_id): """ @@ -2620,7 +2583,7 @@ def calculate_grades_csv(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff', perm='student.can_research') +@require_course_permission(permissions.CAN_RESEARCH) @common_exceptions_400 def problem_grade_report(request, course_id): """ @@ -2641,7 +2604,7 @@ def problem_grade_report(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.CAN_ENROLL) @require_post_params('rolename') def list_forum_members(request, course_id): """ @@ -2713,7 +2676,7 @@ def list_forum_members(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.EMAIL) @require_post_params(send_to="sending to whom", subject="subject line", message="message text") @common_exceptions_400 def send_email(request, course_id): @@ -2789,7 +2752,7 @@ def send_email(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(EDIT_FORUM_ROLES) +@require_course_permission(permissions.EDIT_FORUM_ROLES) @require_post_params( unique_student_identifier="email or username of user to change access", rolename="the forum role", @@ -2882,7 +2845,7 @@ def _display_unit(unit): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(GIVE_STUDENT_EXTENSION) +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @require_post_params('student', 'url', 'due_datetime') def change_due_date(request, course_id): """ @@ -2906,7 +2869,7 @@ def change_due_date(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(GIVE_STUDENT_EXTENSION) +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @require_post_params('student', 'url') def reset_due_date(request, course_id): """ @@ -2935,7 +2898,7 @@ def reset_due_date(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @require_post_params('url') def show_unit_extensions(request, course_id): """ @@ -2950,7 +2913,7 @@ def show_unit_extensions(request, course_id): @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) @require_post_params('student') def show_student_extensions(request, course_id): """ @@ -3017,7 +2980,7 @@ def generate_example_certificates(request, course_id=None): # pylint: disable=u return redirect(_instructor_dash_url(course_key, section='certificates')) -@require_course_permission(ENABLE_CERTIFICATE_GENERATION) +@require_course_permission(permissions.ENABLE_CERTIFICATE_GENERATION) @require_POST def enable_certificate_generation(request, course_id=None): """Enable/disable self-generated certificates for a course. @@ -3037,7 +3000,7 @@ def enable_certificate_generation(request, course_id=None): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM) +@require_course_permission(permissions.ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM) @require_POST def mark_student_can_skip_entrance_exam(request, course_id): """ @@ -3301,7 +3264,7 @@ def get_student(username_or_email, course_key): @transaction.non_atomic_requests @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(GENERATE_CERTIFICATE_EXCEPTIONS) +@require_course_permission(permissions.GENERATE_CERTIFICATE_EXCEPTIONS) @require_POST @common_exceptions_400 def generate_certificate_exceptions(request, course_id, generate_for=None): @@ -3343,7 +3306,7 @@ def generate_certificate_exceptions(request, course_id, generate_for=None): @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(GENERATE_BULK_CERTIFICATE_EXCEPTIONS) +@require_course_permission(permissions.GENERATE_BULK_CERTIFICATE_EXCEPTIONS) @require_POST def generate_bulk_certificate_exceptions(request, course_id): """ diff --git a/lms/djangoapps/instructor/views/gradebook_api.py b/lms/djangoapps/instructor/views/gradebook_api.py index 32f0ed3197..ca8a4f0293 100644 --- a/lms/djangoapps/instructor/views/gradebook_api.py +++ b/lms/djangoapps/instructor/views/gradebook_api.py @@ -6,7 +6,6 @@ which is currently use by ccx and instructor apps. import math -import six from django.contrib.auth.models import User from django.db import transaction from django.urls import reverse @@ -16,9 +15,11 @@ from opaque_keys.edx.keys import CourseKey from lms.djangoapps.courseware.courses import get_course_with_access from edxmako.shortcuts import render_to_response from lms.djangoapps.grades.api import CourseGradeFactory -from lms.djangoapps.instructor.views.api import require_level +from lms.djangoapps.instructor.views.api import require_course_permission from xmodule.modulestore.django import modulestore +from .. import permissions + # Grade book: max students per page MAX_STUDENTS_PER_PAGE_GRADE_BOOK = 20 @@ -101,7 +102,7 @@ def get_grade_book_page(request, course, course_key): @transaction.non_atomic_requests @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.OVERRIDE_GRADES) def spoc_gradebook(request, course_id): """ Show the gradebook for this course: @@ -114,7 +115,7 @@ def spoc_gradebook(request, course_id): return render_to_response('courseware/gradebook.html', { 'page': page, - 'page_url': reverse('spoc_gradebook', kwargs={'course_id': six.text_type(course_key)}), + 'page_url': reverse('spoc_gradebook', kwargs={'course_id': str(course_key)}), 'students': student_info, 'course': course, 'course_id': course_key, diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index aa87e18c5f..f9dbe22828 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -65,6 +65,7 @@ from xmodule.modulestore.django import modulestore from xmodule.tabs import CourseTab from .tools import get_units_with_due_date, title_or_url +from .. import permissions log = logging.getLogger(__name__) @@ -84,7 +85,7 @@ class InstructorDashboardTab(CourseTab): """ Returns true if the specified user has staff access. """ - return bool(user and has_access(user, 'staff', course, course.id)) + return bool(user and user.is_authenticated and user.has_perm(permissions.VIEW_DASHBOARD, course.id)) def show_analytics_dashboard_message(course_key): @@ -120,24 +121,27 @@ def instructor_dashboard_2(request, course_id): 'sales_admin': CourseSalesAdminRole(course_key).has_user(request.user), 'staff': bool(has_access(request.user, 'staff', course)), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), - 'data_researcher': request.user.has_perm('student.can_research', course_key), + 'data_researcher': request.user.has_perm(permissions.CAN_RESEARCH, course_key), } - if not access['staff']: + if not request.user.has_perm(permissions.VIEW_DASHBOARD, course_key): raise Http404() is_white_label = CourseMode.is_white_label(course_key) reports_enabled = configuration_helpers.get_value('SHOW_ECOMMERCE_REPORTS', False) - sections = [ - _section_course_info(course, access), - _section_membership(course, access), - _section_cohort_management(course, access), - _section_discussions_management(course, access), - _section_student_admin(course, access), - _section_data_download(course, access), - ] + sections = [] + if access['staff']: + sections.extend([ + _section_course_info(course, access), + _section_membership(course, access), + _section_cohort_management(course, access), + _section_discussions_management(course, access), + _section_student_admin(course, access), + ]) + if access['data_researcher']: + sections.append(_section_data_download(course, access)) analytics_dashboard_message = None if show_analytics_dashboard_message(course_key): @@ -207,7 +211,7 @@ def instructor_dashboard_2(request, course_id): openassessment_blocks = [ block for block in openassessment_blocks if block.parent is not None ] - if len(openassessment_blocks) > 0: + if len(openassessment_blocks) > 0 and access['staff']: sections.append(_section_open_response_assessment(request, course, openassessment_blocks, access)) disable_buttons = not _is_small_course(course_key) diff --git a/lms/djangoapps/instructor/views/registration_codes.py b/lms/djangoapps/instructor/views/registration_codes.py index 57348574e4..59c307fd85 100644 --- a/lms/djangoapps/instructor/views/registration_codes.py +++ b/lms/djangoapps/instructor/views/registration_codes.py @@ -5,7 +5,6 @@ E-commerce Tab Instructor Dashboard Query Registration Code Status. import logging -import six from django.urls import reverse from django.utils.translation import ugettext as _ from django.views.decorators.cache import cache_control @@ -14,16 +13,18 @@ from opaque_keys.edx.locator import CourseKey from lms.djangoapps.courseware.courses import get_course_by_id from lms.djangoapps.instructor.enrollment import get_email_params, send_mail_to_student -from lms.djangoapps.instructor.views.api import require_level +from lms.djangoapps.instructor.views.api import require_course_permission from shoppingcart.models import CourseRegistrationCode, RegistrationCodeRedemption from student.models import CourseEnrollment from util.json_request import JsonResponse +from .. import permissions + log = logging.getLogger(__name__) @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_REGISTRATION) @require_GET def look_up_registration_code(request, course_id): """ @@ -47,7 +48,7 @@ def look_up_registration_code(request, course_id): reg_code_already_redeemed = RegistrationCodeRedemption.is_registration_code_redeemed(code) - registration_code_detail_url = reverse('registration_code_details', kwargs={'course_id': six.text_type(course_id)}) + registration_code_detail_url = reverse('registration_code_details', kwargs={'course_id': str(course_id)}) return JsonResponse({ 'is_registration_code_exists': True, @@ -58,7 +59,7 @@ def look_up_registration_code(request, course_id): @cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_level('staff') +@require_course_permission(permissions.VIEW_REGISTRATION) @require_POST def registration_code_details(request, course_id): """