From ee3aa4acee5d05e1c05958f43b97fc1f4d59ef77 Mon Sep 17 00:00:00 2001 From: Kaustav Banerjee Date: Mon, 8 Aug 2022 21:08:36 +0530 Subject: [PATCH 01/24] feat: allow switching anonymous user ID hashing algorithm from shake to md5 The hashing algorithm has been changed in cd60646. However, there are Open edX operators who maintain backward compatibility of anonymous user IDs after past rotations of their Django secret key. For them, altering the hashing algorithm was a breaking change that made their analytics inconsistent. (cherry picked from commit 746e4fed1bfeb731b2f6288fe3ba4ea056397fdb) (cherry picked from commit ff6d92fd892dff5e66bbf77eadf9b91ed2189a24) (cherry picked from commit 7245bdc7f73d9693f15acc885bee4e0d00be8245) (cherry picked from commit 6da7f588ddf0c851f49ab2f3829417fb7aec1718) --- cms/envs/common.py | 11 +++++++++++ common/djangoapps/student/models/user.py | 13 +++++++++++-- common/djangoapps/student/tests/tests.py | 11 +++++++++++ lms/envs/common.py | 11 +++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index d8e5105e6720..26d219a368df 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -591,6 +591,17 @@ # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2024-04-10 'BADGES_ENABLED': False, + + # .. toggle_name: FEATURES['ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Whether to enable the legacy MD5 hashing algorithm to generate anonymous user id + # instead of the newer SHAKE128 hashing algorithm + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2022-08-08 + # .. toggle_target_removal_date: None + # .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/30832' + 'ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID': False, } # .. toggle_name: ENABLE_COPPA_COMPLIANCE diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 2a4a9deb3664..cb806fed4467 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -154,12 +154,21 @@ def anonymous_id_for_user(user, course_id): # function: Rotate at will, since the hashes are stored and # will not change. # include the secret key as a salt, and to make the ids unique across different LMS installs. - hasher = hashlib.shake_128() + legacy_hash_enabled = settings.FEATURES.get('ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID', False) + if legacy_hash_enabled: + # Use legacy MD5 algorithm if flag enabled + hasher = hashlib.md5() + else: + hasher = hashlib.shake_128() hasher.update(settings.SECRET_KEY.encode('utf8')) hasher.update(str(user.id).encode('utf8')) if course_id: hasher.update(str(course_id).encode('utf-8')) - anonymous_user_id = hasher.hexdigest(16) + + if legacy_hash_enabled: + anonymous_user_id = hasher.hexdigest() + else: + anonymous_user_id = hasher.hexdigest(16) # pylint: disable=too-many-function-args try: AnonymousUserId.objects.create( diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 9e61095260b6..5d393149c406 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -1064,6 +1064,17 @@ def test_anonymous_id_secret_key_changes_result_in_diff_values_for_same_new_user assert anonymous_id != new_anonymous_id assert self.user == user_by_anonymous_id(new_anonymous_id) + def test_enable_legacy_hash_flag(self): + """Test that different anonymous id returned if ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID enabled.""" + CourseEnrollment.enroll(self.user, self.course.id) + anonymous_id = anonymous_id_for_user(self.user, self.course.id) + with patch.dict(settings.FEATURES, ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID=True): + # Recreate user object to clear cached anonymous id. + self.user = User.objects.get(pk=self.user.id) + AnonymousUserId.objects.filter(user=self.user).filter(course_id=self.course.id).delete() + new_anonymous_id = anonymous_id_for_user(self.user, self.course.id) + assert anonymous_id != new_anonymous_id + @skip_unless_lms @patch('openedx.core.djangoapps.programs.utils.get_programs') diff --git a/lms/envs/common.py b/lms/envs/common.py index 5f8714ca9cf7..629021d67ff2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1085,6 +1085,17 @@ # .. toggle_creation_date: 2024-04-02 # .. toggle_target_removal_date: None 'BADGES_ENABLED': False, + + # .. toggle_name: FEATURES['ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Whether to enable the legacy MD5 hashing algorithm to generate anonymous user id + # instead of the newer SHAKE128 hashing algorithm + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2022-08-08 + # .. toggle_target_removal_date: None + # .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/30832' + 'ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API From 64f5deab99011aea3a03567bb9f4816ecde77230 Mon Sep 17 00:00:00 2001 From: Kshitij Sobti Date: Wed, 24 May 2023 13:58:55 +0530 Subject: [PATCH 02/24] temp: Add configuration option to redirect to external site when TAP account is unlinked (cherry picked from commit e83a8c8f82849644cf95534cde3fe149e4f11916) (cherry picked from commit 0c831dc390f63ea9259668ec400df9c4bccd95c8) (cherry picked from commit c596bf3b100bcc629602f0c371663da2c0b862ac) --- lms/static/js/student_account/views/LoginView.js | 3 +++ openedx/core/djangoapps/user_authn/views/login.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lms/static/js/student_account/views/LoginView.js b/lms/static/js/student_account/views/LoginView.js index 5832f2c088be..9b7ce2e19908 100644 --- a/lms/static/js/student_account/views/LoginView.js +++ b/lms/static/js/student_account/views/LoginView.js @@ -216,6 +216,7 @@ saveError: function(error) { var errorCode; var msg; + var redirectURL; if (error.status === 0) { msg = gettext('An error has occurred. Check your Internet connection and try again.'); } else if (error.status === 500) { @@ -245,6 +246,7 @@ } else if (error.responseJSON !== undefined) { msg = error.responseJSON.value; errorCode = error.responseJSON.error_code; + redirectURL = error.responseJSON.redirect_url; } else { msg = gettext('An unexpected error has occurred.'); } @@ -266,6 +268,7 @@ this.clearFormErrors(); this.renderThirdPartyAuthWarning(); } + window.location.href = redirectURL; } else { this.renderErrors(this.defaultFormErrorsTitle, this.errors); } diff --git a/openedx/core/djangoapps/user_authn/views/login.py b/openedx/core/djangoapps/user_authn/views/login.py index 7d049871266e..85ed25e42895 100644 --- a/openedx/core/djangoapps/user_authn/views/login.py +++ b/openedx/core/djangoapps/user_authn/views/login.py @@ -76,7 +76,7 @@ def _do_third_party_auth(request): try: return pipeline.get_authenticated_user(requested_provider, username, third_party_uid) - except USER_MODEL.DoesNotExist: + except USER_MODEL.DoesNotExist as err: AUDIT_LOG.info( "Login failed - user with username {username} has no social auth " "with backend_name {backend_name}".format( @@ -98,7 +98,13 @@ def _do_third_party_auth(request): ) ) - raise AuthFailedError(message, error_code='third-party-auth-with-no-linked-account') # lint-amnesty, pylint: disable=raise-missing-from + redirect_url = configuration_helpers.get_value('OC_REDIRECT_ON_TPA_UNLINKED_ACCOUNT', None) + + raise AuthFailedError( + message, + error_code='third-party-auth-with-no-linked-account', + redirect_url=redirect_url + ) from err def _get_user_by_email(email): From a0d88065b11f8d182d3c00deb5935880bf0611e1 Mon Sep 17 00:00:00 2001 From: 0x29a Date: Mon, 26 Jun 2023 11:08:26 +0200 Subject: [PATCH 03/24] feat: all eSHE role features squashed in one commit fix: give superusers all studio permissions (cherry picked from commit 8ef55754f4a529cc6b784298320fcdb8b415bd83) (cherry picked from commit 8e281a9535b8317e24f8d2b3b5d840a3eb852865) (cherry picked from commit f55297379276ddcdfd140b43bdc54d19128744d9) (cherry picked from commit 6de7b64ae477bf882c1ae3a97ef2e8940e773a57) 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 5d160c2ecd91e58276dccfd84ca32fb689f46e5e) (cherry picked from commit a21b4f0da1d6eb5240d15aa8b50fd77454b47167) feat: Teaching Assistant role (cherry picked from commit 176de06291aafe4a8f8c9a3a9afdec24cedb332e) (cherry picked from commit 7ef00c0dd265c09a52c71feab9d58a34f7f2c54c) --- common/djangoapps/student/roles.py | 21 +++- common/djangoapps/student/tests/test_roles.py | 4 + lms/djangoapps/instructor/access.py | 4 + lms/djangoapps/instructor/permissions.py | 21 +++- .../tests/views/test_instructor_dashboard.py | 99 +++++++++++++++++++ .../instructor/views/instructor_dashboard.py | 24 ++++- lms/envs/common.py | 20 ++++ .../instructor_dashboard/membership.html | 2 + .../instructor_dashboard_2/membership.html | 37 +++++++ .../instructor_dashboard_2/student_admin.html | 16 ++- 10 files changed, 237 insertions(+), 11 deletions(-) diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py index 971433c9c523..cd7ced4e9fdd 100644 --- a/common/djangoapps/student/roles.py +++ b/common/djangoapps/student/roles.py @@ -211,7 +211,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: @@ -361,6 +361,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""" diff --git a/common/djangoapps/student/tests/test_roles.py b/common/djangoapps/student/tests/test_roles.py index da1aad19a803..816e20fa198b 100644 --- a/common/djangoapps/student/tests/test_roles.py +++ b/common/djangoapps/student/tests/test_roles.py @@ -17,6 +17,8 @@ CourseStaffRole, CourseFinanceAdminRole, CourseSalesAdminRole, + eSHEInstructorRole, + TeachingAssistantRole, LibraryUserRole, CourseDataResearcherRole, GlobalStaff, @@ -199,6 +201,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')), diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py index 9255d113f038..c5bc5fb83370 100644 --- a/lms/djangoapps/instructor/access.py +++ b/lms/djangoapps/instructor/access.py @@ -19,6 +19,8 @@ CourseInstructorRole, CourseLimitedStaffRole, CourseStaffRole, + eSHEInstructorRole, + TeachingAssistantRole, ) from lms.djangoapps.instructor.enrollment import enroll_email, get_email_params from openedx.core.djangoapps.django_comment_common.models import Role @@ -30,6 +32,8 @@ 'instructor': CourseInstructorRole, 'staff': CourseStaffRole, 'limited_staff': CourseLimitedStaffRole, + 'eshe_instructor': eSHEInstructorRole, + 'teaching_assistant': TeachingAssistantRole, 'ccx_coach': CourseCcxCoachRole, 'data_researcher': CourseDataResearcherRole, } diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py index e1a1cbf466f6..a06905a0a35d 100644 --- a/lms/djangoapps/instructor/permissions.py +++ b/lms/djangoapps/instructor/permissions.py @@ -36,6 +36,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') @@ -49,17 +64,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') diff --git a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py index 400e9b556283..67bffae5991c 100644 --- a/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py +++ b/lms/djangoapps/instructor/tests/views/test_instructor_dashboard.py @@ -30,6 +30,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 from lms.djangoapps.instructor.views.gradebook_api import calculate_page_info @@ -38,6 +39,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 @@ -314,6 +316,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 = ( + '' + ) + batch_enrollment = ( + '
' + ) + + 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 = '
' + view_progress = '
' + + # Should not be visible to Teaching Assistants + view_gradebook = '
' + adjust_learner_grade = '
' + adjust_all_learners_grades = '
' + + 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 diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index a068ffdf1c9d..4e976b18932d 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -31,7 +31,10 @@ CourseFinanceAdminRole, CourseInstructorRole, CourseSalesAdminRole, - CourseStaffRole + CourseStaffRole, + eSHEInstructorRole, + TeachingAssistantRole, + strict_role_checking, ) from common.djangoapps.util.json_request import JsonResponse from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled @@ -122,6 +125,8 @@ 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)), @@ -129,6 +134,9 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable '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() @@ -514,7 +522,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 diff --git a/lms/envs/common.py b/lms/envs/common.py index 629021d67ff2..f226330aa541 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1096,6 +1096,26 @@ # .. toggle_target_removal_date: None # .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/30832' 'ENABLE_LEGACY_MD5_HASH_FOR_ANONYMOUS_USER_ID': False, + + # .. toggle_name: FEATURES['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: FEATURES['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 diff --git a/lms/static/js/fixtures/instructor_dashboard/membership.html b/lms/static/js/fixtures/instructor_dashboard/membership.html index 85ab52c9665e..383552f7d83f 100644 --- a/lms/static/js/fixtures/instructor_dashboard/membership.html +++ b/lms/static/js/fixtures/instructor_dashboard/membership.html @@ -11,6 +11,8 @@

Course Team Management

% for form_name, form_value in submit_disabled_cta['form_values'].items(): @@ -76,13 +77,14 @@

${submit_disabled_cta['link_name']} - - + % endif + + - (${submit_disabled_cta['description']}) - + + (${submit_disabled_cta['description']}) + % endif % endif
diff --git a/lms/templates/vert_module.html b/lms/templates/vert_module.html index 0e52e3c7f426..7df5b8b5b46e 100644 --- a/lms/templates/vert_module.html +++ b/lms/templates/vert_module.html @@ -44,7 +44,7 @@

${unit_title}

- % else: + % elif vertical_banner_cta.get('link'):