Skip to content

Commit

Permalink
Merge branch 'open-release/redwood.master' into feat/master/course-ab…
Browse files Browse the repository at this point in the history
…out-page-markup-redwood
  • Loading branch information
ihor-romaniuk authored Jul 31, 2024
2 parents 7bffc2a + 1e0d575 commit c0da4f8
Show file tree
Hide file tree
Showing 36 changed files with 461 additions and 128 deletions.
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ common/test/data/badges/*.png
### Static assets pipeline artifacts
**/*.scssc
lms/static/css/
!lms/static/css/vendor
lms/static/certificates/css/
cms/static/css/
common/static/common/js/vendor/
Expand Down
11 changes: 5 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ bin/

### Static assets pipeline artifacts
*.scssc
lms/static/css/
lms/static/certificates/css/
cms/static/css/
common/static/common/js/vendor/
common/static/common/css/vendor/
lms/static/css
lms/static/certificates/css
cms/static/css
common/static/common/js/vendor
common/static/common/css/vendor
common/static/bundles
webpack-stats.json

Expand All @@ -115,7 +115,6 @@ lms/static/sass/*.css
lms/static/sass/*.css.map
lms/static/certificates/sass/*.css
lms/static/themed_sass/
cms/static/css/
cms/static/sass/*.css
cms/static/sass/*.css.map
cms/static/themed_sass/
Expand Down
1 change: 1 addition & 0 deletions cms/djangoapps/contentstore/rest_api/v1/views/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,5 @@ def get(self, request: Request):

library_context = get_library_context(request)
serializer = LibraryTabSerializer(library_context)

return Response(serializer.data)
5 changes: 3 additions & 2 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,7 @@ def get_home_context(request, no_course=False):
LIBRARY_AUTHORING_MICROFRONTEND_URL,
LIBRARIES_ENABLED,
should_redirect_to_library_authoring_mfe,
user_can_create_library,
user_can_view_create_library_button,
)

active_courses = []
Expand Down Expand Up @@ -1699,7 +1699,8 @@ def get_home_context(request, no_course=False):
'library_authoring_mfe_url': LIBRARY_AUTHORING_MICROFRONTEND_URL,
'taxonomy_list_mfe_url': get_taxonomy_list_url(),
'libraries': libraries,
'show_new_library_button': user_can_create_library(user) and not should_redirect_to_library_authoring_mfe(),
'show_new_library_button': user_can_view_create_library_button(user)
and not should_redirect_to_library_authoring_mfe(),
'user': user,
'request_course_creator_url': reverse('request_course_creator'),
'course_creator_status': _get_course_creator_status(user),
Expand Down
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
CourseStaffRole,
GlobalStaff,
UserBasedRole,
OrgStaffRole
OrgStaffRole,
)
from common.djangoapps.util.json_request import JsonResponse, JsonResponseBadRequest, expect_json
from common.djangoapps.util.string_utils import _has_non_ascii_characters
Expand Down
60 changes: 51 additions & 9 deletions cms/djangoapps/contentstore/views/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseForbidden, HttpResponseNotAllowed
from django.http import Http404, HttpResponseNotAllowed
from django.utils.translation import gettext as _
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
Expand Down Expand Up @@ -69,12 +69,10 @@ def should_redirect_to_library_authoring_mfe():
)


def user_can_create_library(user, org=None):
def user_can_view_create_library_button(user):
"""
Helper method for returning the library creation status for a particular user,
taking into account the value LIBRARIES_ENABLED.
Helper method for displaying the visibilty of the create_library_button.
"""

if not LIBRARIES_ENABLED:
return False
elif user.is_staff:
Expand All @@ -84,8 +82,56 @@ def user_can_create_library(user, org=None):
has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).exists()
has_course_staff_role = UserBasedRole(user=user, role=CourseStaffRole.ROLE).courses_with_role().exists()
has_course_admin_role = UserBasedRole(user=user, role=CourseInstructorRole.ROLE).courses_with_role().exists()
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role
else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
disable_course_creation = settings.FEATURES.get('DISABLE_COURSE_CREATION', False)
if disable_library_creation is not None:
return not disable_library_creation
else:
return not disable_course_creation


def user_can_create_library(user, org):
"""
Helper method for returning the library creation status for a particular user,
taking into account the value LIBRARIES_ENABLED.
if the ENABLE_CREATOR_GROUP value is False, then any user can create a library (in any org),
if library creation is enabled.
if the ENABLE_CREATOR_GROUP value is true, then what a user can do varies by thier role.
Global Staff: can make libraries in any org.
Course Creator Group Members: can make libraries in any org.
Organization Staff: Can make libraries in the organization for which they are staff.
Course Staff: Can make libraries in the organization which has courses of which they are staff.
Course Admin: Can make libraries in the organization which has courses of which they are Admin.
"""
if org is None:
return False
if not LIBRARIES_ENABLED:
return False
elif user.is_staff:
return True
if settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
is_course_creator = get_course_creator_status(user) == 'granted'
has_org_staff_role = org in OrgStaffRole().get_orgs_for_user(user)
has_course_staff_role = (
UserBasedRole(user=user, role=CourseStaffRole.ROLE)
.courses_with_role()
.filter(org=org)
.exists()
)
has_course_admin_role = (
UserBasedRole(user=user, role=CourseInstructorRole.ROLE)
.courses_with_role()
.filter(org=org)
.exists()
)
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role

else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
Expand All @@ -108,12 +154,8 @@ def library_handler(request, library_key_string=None):
raise Http404 # Should never happen because we test the feature in urls.py also

if request.method == 'POST':
if not user_can_create_library(request.user):
return HttpResponseForbidden()

if library_key_string is not None:
return HttpResponseNotAllowed(("POST",))

return _create_library(request)

else:
Expand Down
39 changes: 25 additions & 14 deletions cms/djangoapps/contentstore/views/tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,55 +59,66 @@ def setUp(self):
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", False)
def test_library_creator_status_libraries_not_enabled(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
self.assertEqual(user_can_create_library(nostaff_user), False)
self.assertEqual(user_can_create_library(nostaff_user, None), False)

# When creator group is disabled, non-staff users can create libraries
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_no_course_creator_role(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
self.assertEqual(user_can_create_library(nostaff_user), True)
self.assertEqual(user_can_create_library(nostaff_user, 'An Org'), True)

# When creator group is enabled, Non staff users cannot create libraries
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_for_enabled_creator_group_setting_for_non_staff_users(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
self.assertEqual(user_can_create_library(nostaff_user), False)
self.assertEqual(user_can_create_library(nostaff_user, None), False)

# Global staff can create libraries
# Global staff can create libraries for any org, even ones that don't exist.
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_is_staff_user(self):
self.assertEqual(user_can_create_library(self.user), True)
print(self.user.is_staff)
self.assertEqual(user_can_create_library(self.user, 'aNyOrg'), True)

# When creator groups are enabled, global staff can create libraries
# Global staff can create libraries for any org, but an org has to be supplied.
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_is_staff_user_no_org(self):
print(self.user.is_staff)
self.assertEqual(user_can_create_library(self.user, None), False)

# When creator groups are enabled, global staff can create libraries in any org
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_for_enabled_creator_group_setting_with_is_staff_user(self):
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
self.assertEqual(user_can_create_library(self.user), True)
self.assertEqual(user_can_create_library(self.user, 'RandomOrg'), True)

# When creator groups are enabled, course creators can create libraries
# When creator groups are enabled, course creators can create libraries in any org.
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_course_creator_role_for_enabled_creator_group_setting(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
grant_course_creator_status(self.user, nostaff_user)
self.assertEqual(user_can_create_library(nostaff_user), True)
self.assertEqual(user_can_create_library(nostaff_user, 'soMeRandOmoRg'), True)

# When creator groups are enabled, course staff members can create libraries
# but only in the org they are course staff for.
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_course_staff_role_for_enabled_creator_group_setting(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
auth.add_users(self.user, CourseStaffRole(self.course.id), nostaff_user)
self.assertEqual(user_can_create_library(nostaff_user), True)
self.assertEqual(user_can_create_library(nostaff_user, self.course.org), True)
self.assertEqual(user_can_create_library(nostaff_user, 'SomEOtherOrg'), False)

# When creator groups are enabled, course instructor members can create libraries
# but only in the org they are course staff for.
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
def test_library_creator_status_with_course_instructor_role_for_enabled_creator_group_setting(self):
_, nostaff_user = self.create_non_staff_authed_user_client()
with mock.patch.dict('django.conf.settings.FEATURES', {"ENABLE_CREATOR_GROUP": True}):
auth.add_users(self.user, CourseInstructorRole(self.course.id), nostaff_user)
self.assertEqual(user_can_create_library(nostaff_user), True)
self.assertEqual(user_can_create_library(nostaff_user, self.course.org), True)
self.assertEqual(user_can_create_library(nostaff_user, 'SomEOtherOrg'), False)

@ddt.data(
(False, False, True),
Expand All @@ -131,7 +142,7 @@ def test_library_creator_status_settings(self, disable_course, disable_library,
"DISABLE_LIBRARY_CREATION": disable_library
}
):
self.assertEqual(user_can_create_library(nostaff_user), expected_status)
self.assertEqual(user_can_create_library(nostaff_user, 'SomEOrg'), expected_status)

@mock.patch.dict('django.conf.settings.FEATURES', {'DISABLE_COURSE_CREATION': True})
@mock.patch("cms.djangoapps.contentstore.views.library.LIBRARIES_ENABLED", True)
Expand All @@ -140,7 +151,7 @@ def test_library_creator_status_with_no_course_creator_role_and_disabled_nonstaf
Ensure that `DISABLE_COURSE_CREATION` feature works with libraries as well.
"""
nostaff_client, nostaff_user = self.create_non_staff_authed_user_client()
self.assertFalse(user_can_create_library(nostaff_user))
self.assertFalse(user_can_create_library(nostaff_user, 'SomEOrg'))

# To be explicit, this user can GET, but not POST
get_response = nostaff_client.get_json(LIBRARY_REST_URL)
Expand Down Expand Up @@ -251,7 +262,7 @@ def test_lib_create_permission_course_staff_role(self):
auth.add_users(self.user, CourseStaffRole(self.course.id), ns_user)
self.assertTrue(auth.user_has_role(ns_user, CourseStaffRole(self.course.id)))
response = self.client.ajax_post(LIBRARY_REST_URL, {
'org': 'org', 'library': 'lib', 'display_name': "New Library",
'org': self.course.org, 'library': 'lib', 'display_name': "New Library",
})
self.assertEqual(response.status_code, 200)

Expand Down
16 changes: 11 additions & 5 deletions common/djangoapps/student/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
OrgContentCreatorRole,
OrgInstructorRole,
OrgLibraryUserRole,
OrgStaffRole
OrgStaffRole,
)

# Studio permissions:
Expand Down Expand Up @@ -95,17 +95,23 @@ def get_user_permissions(user, course_key, org=None):
return all_perms
# HACK: Limited Staff should not have studio read access. However, since many LMS views depend on the
# `has_course_author_access` check and `course_author_access_required` decorator, we have to allow write access
# until the permissions become more granular. For example, there could be STUDIO_VIEW_COHORTS and
# STUDIO_EDIT_COHORTS specifically for the cohorts endpoint, which is used to display the "Cohorts" tab of the
# Instructor Dashboard.
# by returning STUDIO_EDIT_CONTENT, if the request is made from LMS, until the permissions become more granular.
# For example, there could be STUDIO_VIEW_COHORTS and STUDIO_EDIT_COHORTS specifically for the cohorts endpoint,
# which is used to display the "Cohorts" tab of the Instructor Dashboard. If the request is made from the CMS,
# then STUDIO_NO_PERMISSIONS is returned instead.
# The permissions matrix from the RBAC project (https://github.com/openedx/platform-roadmap/issues/246) shows that
# 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)):
return STUDIO_EDIT_CONTENT
if settings.SERVICE_VARIANT == 'lms':
return STUDIO_EDIT_CONTENT
else:
return STUDIO_NO_PERMISSIONS

# Staff have all permissions except EDIT_ROLES:
if OrgStaffRole(org=org).has_user(user) or (course_key and user_has_role(user, CourseStaffRole(course_key))):
return STUDIO_VIEW_USERS | STUDIO_EDIT_CONTENT | STUDIO_VIEW_CONTENT

# Otherwise, for libraries, users can view only:
if course_key and isinstance(course_key, LibraryLocator):
if OrgLibraryUserRole(org=org).has_user(user) or user_has_role(user, LibraryUserRole(course_key)):
Expand Down
2 changes: 1 addition & 1 deletion common/djangoapps/student/role_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ def get_course_roles(user: User) -> list[CourseAccessRole]:
"""
# pylint: disable=protected-access
role_cache = get_role_cache(user)
return list(role_cache._roles)
return list(role_cache.all_roles_set)
Loading

0 comments on commit c0da4f8

Please sign in to comment.