Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fb693bf
feat: allow switching anonymous user ID hashing algorithm from shake …
kaustavb12 Aug 8, 2022
22b4445
temp: Add configuration option to redirect to external site when TAP …
xitij2000 May 24, 2023
bfa2081
feat: all eSHE role features squashed in one commit
0x29a Jun 26, 2023
5fb5130
feat: DEFAULT_COURSE_INVITATION_ONLY allow for invitation_only new co…
viadanna Apr 9, 2024
9e735ad
feat: add a feature flag to disable dates tab for all courses
Cup0fCoffee Apr 15, 2024
ad27ea7
feat: tpa automatic logout with a single redirect
CefBoud Apr 16, 2024
11688bd
feat: Allow trusted apps to perform cookie login.
Kelketek Sep 6, 2023
f7bcad0
fix: prevent redirects to /undefined after saml auth
tecoholic Aug 13, 2024
c4d4814
feat: in-context discussion for units can be disabled by default (#676)
kaustavb12 Sep 2, 2024
ecaa84f
fix: unhide discussion tab when enabling it (#677)
viadanna Sep 4, 2024
36d5889
fix: unhide discussion tab when enabling it (#677)
viadanna Sep 4, 2024
7fcc1ad
feat: add has_course_author_access to CourseHomeMetadataView response…
0x29a Sep 10, 2024
5c74009
feat: allow Bearer auth for sequence metadata
0x29a Oct 23, 2024
b2d3651
feat: User agreements API for generic agreement records
xitij2000 Nov 20, 2024
735a77c
fix: use a single 'provider_type' key for storing discussion provider…
0x29a Dec 24, 2024
ea05de5
fix: remove incorrect key type when filtering inaccessible blocks
Agrendalath Sep 7, 2024
1d2384f
chore: bump ora2 fork
viadanna Mar 7, 2025
793b7b1
Merge pull request #35713 from openedx/feanil/ubuntu-24.04
feanil Nov 7, 2024
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
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,11 @@ def sync_discussion_settings(course_key, user):
if (
ENABLE_NEW_STRUCTURE_DISCUSSIONS.is_enabled()
and not course.discussions_settings['provider_type'] == Provider.OPEN_EDX
and not course.discussions_settings['provider'] == Provider.OPEN_EDX
):
LOGGER.info(f"New structure is enabled, also updating {course_key} to use new provider")
course.discussions_settings['enable_graded_units'] = False
course.discussions_settings['unit_level_visibility'] = True
course.discussions_settings['provider'] = Provider.OPEN_EDX
course.discussions_settings['provider_type'] = Provider.OPEN_EDX
modulestore().update_item(course, user.id)

Expand Down
10 changes: 10 additions & 0 deletions cms/djangoapps/contentstore/tests/test_contentstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,16 @@ def test_create_course_with_unicode_in_id_disabled(self):
self.course_data['run'] = '����������'
self.assert_create_course_failed(error_message)

@override_settings(DEFAULT_COURSE_INVITATION_ONLY=True)
def test_create_course_invitation_only(self):
"""
Test new course creation with setting: DEFAULT_COURSE_INVITATION_ONLY=True.
"""
test_course_data = self.assert_created_course()
course_id = _get_course_id(self.store, test_course_data)
course = self.store.get_course(course_id)
self.assertEqual(course.invitation_only, True)

def assert_course_permission_denied(self):
"""
Checks that the course did not get created due to a PermissionError.
Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -1006,8 +1006,9 @@ def create_new_course_in_store(store, user, org, number, run, fields):

# Set default language from settings and enable web certs
fields.update({
'language': getattr(settings, 'DEFAULT_COURSE_LANGUAGE', 'en'),
'cert_html_view_enabled': True,
'invitation_only': getattr(settings, 'DEFAULT_COURSE_INVITATION_ONLY', False),
'language': getattr(settings, 'DEFAULT_COURSE_LANGUAGE', 'en'),
})

with modulestore().default_store(store):
Expand Down
23 changes: 23 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,25 @@
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2024-04-10
'BADGES_ENABLED': False,

# .. toggle_name: FEATURES['IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT']
# .. toggle_implementation: DjangoSetting
# .. toggle_default: True
# .. toggle_description: Set to False to not enable in-context discussion for units by default.
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2024-09-02
'IN_CONTEXT_DISCUSSION_ENABLED_DEFAULT': True,

# .. 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
Expand Down Expand Up @@ -2943,3 +2962,7 @@ def _should_send_learning_badge_events(settings):
# .. for now it wil impact country listing in auth flow and user profile.
# .. eg ['US', 'CA']
DISABLED_COUNTRIES = []

############## Default value for invitation_only when creating courses ##############

DEFAULT_COURSE_INVITATION_ONLY = False
14 changes: 8 additions & 6 deletions common/djangoapps/student/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def user_has_role(user, role):
return False


def get_user_permissions(user, course_key, org=None):
def get_user_permissions(user, course_key, org=None, service_variant=None):
"""
Get the bitmask of permissions that this user has in the given course context.
Can also set course_key=None and pass in an org to get the user's
Expand Down Expand Up @@ -103,7 +103,7 @@ def get_user_permissions(user, course_key, org=None):
# the LMS and Studio permissions will be separated as a part of this project. Once this is done (and this code is
# not removed during its implementation), we can replace the Limited Staff permissions with more granular ones.
if course_key and user_has_role(user, CourseLimitedStaffRole(course_key)):
if settings.SERVICE_VARIANT == 'lms':
if (service_variant or settings.SERVICE_VARIANT) == 'lms':
return STUDIO_EDIT_CONTENT
else:
return STUDIO_NO_PERMISSIONS
Expand All @@ -119,7 +119,7 @@ def get_user_permissions(user, course_key, org=None):
return STUDIO_NO_PERMISSIONS


def has_studio_write_access(user, course_key):
def has_studio_write_access(user, course_key, service_variant=None):
"""
Return True if user has studio write access to the given course.
Note that the CMS permissions model is with respect to courses.
Expand All @@ -131,15 +131,17 @@ def has_studio_write_access(user, course_key):

:param user:
:param course_key: a CourseKey
:param service_variant: the variant of the service (lms or cms). Permissions may differ between the two,
see the comment in get_user_permissions for more details.
"""
return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key))
return bool(STUDIO_EDIT_CONTENT & get_user_permissions(user, course_key, service_variant=service_variant))


def has_course_author_access(user, course_key):
def has_course_author_access(user, course_key, service_variant=None):
"""
Old name for has_studio_write_access
"""
return has_studio_write_access(user, course_key)
return has_studio_write_access(user, course_key, service_variant)


def has_studio_advanced_settings_access(user):
Expand Down
13 changes: 11 additions & 2 deletions common/djangoapps/student/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 20 additions & 1 deletion common/djangoapps/student/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"""
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 @@ -17,6 +17,8 @@
CourseStaffRole,
CourseFinanceAdminRole,
CourseSalesAdminRole,
eSHEInstructorRole,
TeachingAssistantRole,
LibraryUserRole,
CourseDataResearcherRole,
GlobalStaff,
Expand Down Expand Up @@ -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')),
Expand Down
11 changes: 11 additions & 0 deletions common/djangoapps/student/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ class CourseHomeMetadataSerializer(VerifiedModeSerializer):
can_view_certificate = serializers.BooleanField()
course_modes = CourseModeSerrializer(many=True)
is_new_discussion_sidebar_view_enabled = serializers.BooleanField()
has_course_author_access = serializers.BooleanField()
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole
from common.djangoapps.student.roles import (
CourseBetaTesterRole,
CourseInstructorRole,
CourseLimitedStaffRole,
CourseStaffRole
)
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
from lms.djangoapps.courseware.toggles import (
Expand Down Expand Up @@ -247,3 +252,32 @@ def test_discussion_tab_visible(self, visible):
assert 'discussion' in tab_ids
else:
assert 'discussion' not in tab_ids

@ddt.data(
{
'course_team_role': None,
'has_course_author_access': False
},
{
'course_team_role': CourseBetaTesterRole,
'has_course_author_access': False
},
{
'course_team_role': CourseStaffRole,
'has_course_author_access': True
},
{
'course_team_role': CourseLimitedStaffRole,
'has_course_author_access': False
},
)
@ddt.unpack
def test_has_course_author_access_for_staff_roles(self, course_team_role, has_course_author_access):
CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED)

if course_team_role:
course_team_role(self.course.id).add_users(self.user)

response = self.client.get(self.url)
assert response.status_code == 200
assert response.data['has_course_author_access'] == has_course_author_access
7 changes: 7 additions & 0 deletions lms/djangoapps/course_home_api/course_metadata/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict

from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.course_api.api import course_detail
from lms.djangoapps.course_goals.models import UserActivity
Expand Down Expand Up @@ -140,6 +141,12 @@ def get(self, request, *args, **kwargs):
'can_view_certificate': certificates_viewable_for_course(course),
'course_modes': course_modes,
'is_new_discussion_sidebar_view_enabled': new_discussion_sidebar_view_is_enabled(course_key),
# We check the course author access in the context of CMS here because this field is used
# to determine whether the user can access the course authoring tools in the CMS.
# This is a temporary solution until the course author role is split into "Course Author" and
# "Course Editor" as described in the permission matrix here:
# https://github.com/openedx/platform-roadmap/issues/246
'has_course_author_access': has_course_author_access(request.user, course_key, 'cms'),
}
context = self.get_serializer_context()
context['course'] = course
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/course_home_api/outline/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ def filter_inaccessible_blocks(self, course_blocks, course_key):
for section_data in course_sections:
section_data['children'] = self.get_accessible_sequences(
user_course_outline,
section_data.get('children', ['completion'])
section_data.get('children', [])
)
accessible_sequence_ids = {str(usage_key) for usage_key in user_course_outline.accessible_sequences}
for sequence_data in section_data['children']:
Expand Down
6 changes: 6 additions & 0 deletions lms/djangoapps/courseware/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ def link_func(course, _reverse_func):
tab_dict['link_func'] = link_func
super().__init__(tab_dict)

@classmethod
def is_enabled(cls, course, user=None):
if settings.FEATURES.get('DISABLE_DATES_TAB'):
return False
return super().is_enabled(course, user)


def get_course_tab_list(user, course):
"""
Expand Down
13 changes: 13 additions & 0 deletions lms/djangoapps/courseware/tests/test_tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,16 @@ def test_singular_dates_tab(self):
if tab.type == 'dates':
num_dates_tabs += 1
assert num_dates_tabs == 1

def test_dates_tab_is_enabled_by_default(self):
"""Test dates tab is enabled by default."""
tab = DatesTab({'type': DatesTab.type, 'name': 'dates'})
user = self.create_mock_user()
assert self.is_tab_enabled(tab, self.course, user)

@patch.dict("django.conf.settings.FEATURES", {"DISABLE_DATES_TAB": True})
def test_dates_tab_disabled_by_feature_flag(self):
"""Test dates tab is disabled by the feature flag."""
tab = DatesTab({'type': DatesTab.type, 'name': 'dates'})
user = self.create_mock_user()
assert not self.is_tab_enabled(tab, self.course, user)
4 changes: 4 additions & 0 deletions lms/djangoapps/instructor/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +32,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 @@ -38,6 +38,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 @@ -51,17 +66,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
Loading
Loading