Merge pull request #23728 from edx/dcs/dashboard

Refactor instructor dashboard access
This commit is contained in:
Dave St.Germain
2020-04-27 10:25:20 -04:00
committed by GitHub
9 changed files with 172 additions and 149 deletions

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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

View File

@@ -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')

View File

@@ -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"),
)

View File

@@ -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):
"""

View File

@@ -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,

View File

@@ -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)

View File

@@ -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):
"""