Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: all eSHE role features squashed in one commit
fix: give superusers all studio permissions

(cherry picked from commit 8ef55754f4a529cc6b784298320fcdb8b415bd83)
(cherry picked from commit 8e281a9)
(cherry picked from commit f552973)
(cherry picked from commit 6de7b64)

feat: eSHE Instructor role

Adds the eSHE Instructor role, which inherits Course Staff permissions,
but isn't able to enroll / un-enroll students and can't assing course
team roles unless in combination with Course Staff / Instructor /
Discussion admin roles.

(cherry picked from commit 5d160c2)
(cherry picked from commit a21b4f0)

feat: Teaching Assistant role

(cherry picked from commit 176de06)
(cherry picked from commit 7ef00c0)
(cherry picked from commit a0d8806)
(cherry picked from commit 45f6593)
(cherry picked from commit 7e5756a)
(cherry picked from commit 404226e)
  • Loading branch information
0x29a authored and xitij2000 committed Aug 11, 2026
commit 036cd1aceb7ddde9e549ab57ee141944e3476ce3
21 changes: 20 additions & 1 deletion common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ class GlobalStaff(AccessRole):
The global staff role
"""
def has_user(self, user):
return bool(user and user.is_staff)
return bool(user and (user.is_superuser or user.is_staff))

def add_users(self, *users):
for user in users:
Expand Down Expand Up @@ -735,6 +735,25 @@ class CourseLimitedStaffRole(CourseStaffRole):
BASE_ROLE = CourseStaffRole.ROLE


@register_access_role
class eSHEInstructorRole(CourseStaffRole):
"""A Staff member of a course without access to the membership tab and enrollment-related operations."""

ROLE = 'eshe_instructor'
BASE_ROLE = CourseStaffRole.ROLE


@register_access_role
class TeachingAssistantRole(CourseStaffRole):
"""
A Staff member of a course without access to the membership tab, enrollment-related operations and
grade-related operations.
"""

ROLE = 'teaching_assistant'
BASE_ROLE = CourseStaffRole.ROLE


@register_access_role
class CourseInstructorRole(CourseRole):
"""A course Instructor"""
Expand Down
4 changes: 4 additions & 0 deletions common/djangoapps/student/tests/test_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
OrgInstructorRole,
OrgStaffRole,
RoleCache,
TeachingAssistantRole,
eSHEInstructorRole,
get_authz_compat_course_access_roles_for_user,
get_role_cache_key_for_course,
)
Expand Down Expand Up @@ -391,6 +393,8 @@ class RoleCacheTestCase(TestCase): # lint-amnesty, pylint: disable=missing-clas
ROLES = (
(CourseStaffRole(IN_KEY), ('staff', IN_KEY, 'edX')),
(CourseLimitedStaffRole(IN_KEY), ('limited_staff', IN_KEY, 'edX')),
(eSHEInstructorRole(IN_KEY), ('eshe_instructor', IN_KEY, 'edX')),
(TeachingAssistantRole(IN_KEY), ('teaching_assistant', IN_KEY, 'edX')),
(CourseInstructorRole(IN_KEY), ('instructor', IN_KEY, 'edX')),
(OrgStaffRole(IN_KEY.org), ('staff', None, 'edX')),
(CourseFinanceAdminRole(IN_KEY), ('finance_admin', IN_KEY, 'edX')),
Expand Down
4 changes: 4 additions & 0 deletions lms/djangoapps/instructor/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
CourseInstructorRole,
CourseLimitedStaffRole,
CourseStaffRole,
TeachingAssistantRole,
eSHEInstructorRole,
)
from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params
from openedx.core.djangoapps.django_comment_common.models import (
Expand All @@ -39,6 +41,8 @@
'instructor': CourseInstructorRole,
'staff': CourseStaffRole,
'limited_staff': CourseLimitedStaffRole,
'eshe_instructor': eSHEInstructorRole,
'teaching_assistant': TeachingAssistantRole,
'ccx_coach': CourseCcxCoachRole,
'data_researcher': CourseDataResearcherRole,
}
Expand Down
21 changes: 18 additions & 3 deletions lms/djangoapps/instructor/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@
VIEW_ENROLLMENTS = 'instructor.view_enrollments'
VIEW_FORUM_MEMBERS = 'instructor.view_forum_members'

# Due to how the roles iheritance is implemented currently, eshe_instructor and teaching_assistant have implicit
# staff access, but unlike staff, they shouldn't be able to enroll and do grade-related operations as per client's
# requirements. At the same time, all other staff-derived roles, like Limited Staff, should be able to enroll students.
# This solution is far from perfect, but it's probably the best we can do untill the roles system is reworked.
_is_teaching_assistant = HasRolesRule('teaching_assistant')
_is_eshe_instructor = HasRolesRule('eshe_instructor')
_is_eshe_instructor_or_teaching_assistant = _is_teaching_assistant | _is_eshe_instructor
is_staff_but_not_teaching_assistant = (
(_is_teaching_assistant & HasAccessRule('staff', strict=True)) |
(~_is_teaching_assistant & HasAccessRule('staff'))
)
is_staff_but_not_eshe_instructor_or_teaching_assistant = (
(_is_eshe_instructor_or_teaching_assistant & HasAccessRule('staff', strict=True)) |
(~_is_eshe_instructor_or_teaching_assistant & HasAccessRule('staff'))
)

perms[ALLOW_STUDENT_TO_BYPASS_ENTRANCE_EXAM] = HasAccessRule('staff')
perms[ASSIGN_TO_COHORTS] = HasAccessRule('staff')
Expand All @@ -57,17 +72,17 @@
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[GIVE_STUDENT_EXTENSION] = is_staff_but_not_teaching_assistant
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
# HasAccessRule('staff') also includes course staff
perms[CAN_RESEARCH] = is_staff | HasRolesRule('data_researcher')
perms[CAN_ENROLL] = HasAccessRule('staff')
perms[CAN_ENROLL] = is_staff_but_not_eshe_instructor_or_teaching_assistant
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[OVERRIDE_GRADES] = is_staff_but_not_teaching_assistant
perms[SHOW_TASKS] = HasAccessRule('staff') | HasRolesRule('data_researcher')
perms[EMAIL] = HasAccessRule('staff')
perms[RESCORE_EXAMS] = HasAccessRule('instructor')
Expand Down
99 changes: 99 additions & 0 deletions lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from lms.djangoapps.courseware.tabs import get_course_tab_list
from lms.djangoapps.courseware.tests.factories import StudentModuleFactory
from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from lms.djangoapps.grades.config.waffle import WRITABLE_GRADEBOOK
from lms.djangoapps.instructor.toggles import DATA_DOWNLOAD_V2, LEGACY_INSTRUCTOR_DASHBOARD
from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info
Expand All @@ -39,6 +40,7 @@
ENABLE_PAGES_AND_RESOURCES_MICROFRONTEND,
OVERRIDE_DISCUSSION_LEGACY_SETTINGS_FLAG,
)
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
Expand Down Expand Up @@ -323,6 +325,103 @@ def test_membership_reason_field_visibility(self, enbale_reason_field):
else:
self.assertNotContains(response, reason_field)

@ddt.data('eshe_instructor', 'teaching_assistant')
def test_membership_tab_content(self, role):
"""
Verify that eSHE Instructors and Teaching Assistants don't have access to membership tab and
work correctly with other roles.
"""

membership_section = (
'<li class="nav-item">'
'<button type="button" class="btn-link membership" data-section="membership">'
'Membership'
'</button>'
'</li>'
)
batch_enrollment = (
'<fieldset class="batch-enrollment membership-section">'
)

user = UserFactory.create()
self.client.login(username=user.username, password=self.TEST_PASSWORD)

# eSHE Instructors / Teaching Assistants shouldn't have access to membership tab
CourseAccessRoleFactory(
course_id=self.course.id,
user=user,
role=role,
org=self.course.id.org
)
response = self.client.get(self.url)
self.assertNotContains(response, membership_section)

# However if combined with forum_admin, they should have access to this
# tab, but not to batch enrollment
forum_admin_role = RoleFactory(name=FORUM_ROLE_ADMINISTRATOR, course_id=self.course.id)
forum_admin_role.users.add(user)
response = self.client.get(self.url)
self.assertContains(response, membership_section)
self.assertNotContains(response, batch_enrollment)

# Combined with course staff, should have union of all three roles
# permissions sets
CourseAccessRoleFactory(
course_id=self.course.id,
user=user,
role='staff',
org=self.course.id.org
)
response = self.client.get(self.url)
self.assertContains(response, membership_section)
self.assertContains(response, batch_enrollment)

def test_student_admin_tab_content(self):
"""
Verify that Teaching Assistants don't have access to the gradebook-related sections
of the student admin tab.
"""

# Should be visible to Teaching Assistants
view_enrollment_status = '<div class="student-enrollment-status-container action-type-container">'
view_progress = '<div class="student-progress-container action-type-container">'

# Should not be visible to Teaching Assistants
view_gradebook = '<div class="action-type-container ">'
adjust_learner_grade = '<div class="student-grade-container action-type-container ">'
adjust_all_learners_grades = '<div class="course-specific-container action-type-container ">'

user = UserFactory.create()
self.client.login(username=user.username, password=self.TEST_PASSWORD)

# Teaching Assistants shouldn't have access to the gradebook-related sections
CourseAccessRoleFactory(
course_id=self.course.id,
user=user,
role='teaching_assistant',
org=self.course.id.org
)
response = self.client.get(self.url)
self.assertContains(response, view_enrollment_status)
self.assertContains(response, view_progress)
self.assertNotContains(response, view_gradebook)
self.assertNotContains(response, adjust_learner_grade)
self.assertNotContains(response, adjust_all_learners_grades)

# However if combined with instructor, they should have access to all sections
CourseAccessRoleFactory(
course_id=self.course.id,
user=user,
role='instructor',
org=self.course.id.org
)
response = self.client.get(self.url)
self.assertContains(response, view_enrollment_status)
self.assertContains(response, view_progress)
self.assertContains(response, view_gradebook)
self.assertContains(response, adjust_learner_grade)
self.assertContains(response, adjust_all_learners_grades)

def test_student_admin_staff_instructor(self):
"""
Verify that staff users are not able to see course-wide options, while still
Expand Down
22 changes: 21 additions & 1 deletion lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
CourseInstructorRole,
CourseSalesAdminRole,
CourseStaffRole,
TeachingAssistantRole,
eSHEInstructorRole,
strict_role_checking,
)
from common.djangoapps.util.json_request import JsonResponse
from common.djangoapps.util.proctoring import requires_escalation_email
Expand Down Expand Up @@ -146,13 +149,18 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
access = {
'admin': request.user.is_staff,
'instructor': bool(has_access(request.user, 'instructor', course)),
'eshe_instructor': eSHEInstructorRole(course_key).has_user(request.user),
'teaching_assistant': TeachingAssistantRole(course_key).has_user(request.user),
'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user),
'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(permissions.CAN_RESEARCH, course_key),
}

with strict_role_checking():
access['explicit_staff'] = bool(has_access(request.user, 'staff', course))

if not request.user.has_perm(permissions.VIEW_DASHBOARD, course_key):
raise Http404()

Expand Down Expand Up @@ -546,7 +554,19 @@ def _section_membership(course, access):
'update_forum_role_membership',
kwargs={'course_id': str(course_key)}
),
'is_reason_field_enabled': configuration_helpers.get_value('ENABLE_MANUAL_ENROLLMENT_REASON_FIELD', False)
'is_reason_field_enabled': configuration_helpers.get_value('ENABLE_MANUAL_ENROLLMENT_REASON_FIELD', False),

# Membership section should be hidden for eSHE instructors.
# Since they get Course Staff role implicitly, we need to hide this
# section if the user doesn't have the Course Staff role set explicitly
# or have the Discussion Admin role.
'is_hidden': (
not access['forum_admin']
and (
(access['eshe_instructor'] or access['teaching_assistant'])
and not access['explicit_staff']
)
),
}
return section_data

Expand Down
20 changes: 20 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,26 @@
# .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/30832'
ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID = False

# .. toggle_name: settings.ENABLE_ESHE_INSTRUCTOR_ROLE
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Whether to enable the ESHE Instructor role
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2023-07-31
# .. toggle_target_removal_date: None
# .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/561/files'
ENABLE_ESHE_INSTRUCTOR_ROLE = False

# .. toggle_name: settings.ENABLE_TEACHING_ASSISTANT_ROLE
# .. toggle_implementation: DjangoSetting
# .. toggle_default: False
# .. toggle_description: Whether to enable the Teaching Assistant role
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2024-02-12
# .. toggle_target_removal_date: None
# .. toggle_tickets: 'https://github.com/open-craft/edx-platform/pull/632/files'
ENABLE_TEACHING_ASSISTANT_ROLE = False

# Specifies extra XBlock fields that should available when requested via the Course Blocks API
# Should be a list of tuples of (block_type, field_name), where block_type can also be "*" for all block types.
# e.g. COURSE_BLOCKS_API_EXTRA_FIELDS = [ ('course', 'other_course_settings'), ("problem", "weight") ]
Expand Down
2 changes: 2 additions & 0 deletions lms/static/js/fixtures/instructor_dashboard/membership.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ <h3 class="hd hd-3">Course Team Management</h3>
<select id="member-lists-selector" class="member-lists-selector">
<option value="staff">Staff</option>
<option value="limited_staff">Limited Staff</option>
<option value="eshe_instructor">eSHE Instructor</option>
<option value="teaching_assistant">Teaching Assistant</option>
<option value="instructor">Admin</option>
<option value="beta">Beta Testers</option>
<option value="Administrator">Discussion Admins</option>
Expand Down
37 changes: 37 additions & 0 deletions lms/templates/instructor/instructor_dashboard_2/membership.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
from django.utils.translation import pgettext
from openedx.core.djangolib.markup import HTML, Text
%>

<%
eshe_instructor_or_teaching_assistant_access = section_data['access']['eshe_instructor'] or section_data['access']['teaching_assistant']
membership_section_visible = not eshe_instructor_or_teaching_assistant_access or section_data['access']['explicit_staff']
%>

% if membership_section_visible:
<fieldset class="batch-enrollment membership-section">
<legend id="heading-batch-enrollment" class="hd hd-3">${_("Batch Enrollment")}</legend>
<label>
Expand Down Expand Up @@ -92,6 +99,8 @@ <h3 class="hd hd-3">${_("Register/Enroll Students")}</h3>
</div>
%endif

%endif

<hr class="divider" />

%if section_data['access']['instructor']:
Expand Down Expand Up @@ -193,6 +202,34 @@ <h3 class="hd hd-3">${_("Course Team Management")}</h3>
data-add-button-label="${_("Add Limited Staff")}"
></div>

%if settings.FEATURES.get('ENABLE_ESHE_INSTRUCTOR_ROLE', None):
<div class="auth-list-container"
data-rolename="eshe_instructor"
data-display-name="${_("eSHE Instructor")}"
data-info-text="
${_("Course team members with the eSHE Instructor role help you manage your course. "
"eSHE Instructor permissions are similar to Staff, but they can't assign course team roles and "
"can't enroll and unenroll learners")}"
data-list-endpoint="${ section_data['list_course_role_members_url'] }"
data-modify-endpoint="${ section_data['modify_access_url'] }"
data-add-button-label="${_("Add eSHE Instructor")}"
></div>
%endif

%if settings.FEATURES.get('ENABLE_TEACHING_ASSISTANT_ROLE', None):
<div class="auth-list-container"
data-rolename="teaching_assistant"
data-display-name="${_("Teaching Assistant")}"
data-info-text="
${_("Course team members with the Teaching Assistant role help you manage your course. "
"Teaching Assistant permissions are similar to Staff, but they can't assign course team roles, "
"enroll and unenroll learners and view or override grades")}"
data-list-endpoint="${ section_data['list_course_role_members_url'] }"
data-modify-endpoint="${ section_data['modify_access_url'] }"
data-add-button-label="${_("Add Teaching Assistant")}"
></div>
%endif%

## Note that "Admin" is identified as "Instructor" in the Django admin panel.
<div class="auth-list-container"
data-rolename="instructor"
Expand Down
Loading