Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
117 changes: 117 additions & 0 deletions cms/djangoapps/contentstore/tests/test_course_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ccx_keys.locator import CCXLocator
from django.test import RequestFactory
from opaque_keys.edx.locations import CourseLocator
from openedx_authz.api.data import OrgCourseOverviewGlobData
from openedx_authz.api.users import assign_role_to_user_in_scope
from openedx_authz.constants.roles import COURSE_DATA_RESEARCHER, COURSE_EDITOR, COURSE_STAFF

Expand Down Expand Up @@ -715,3 +716,119 @@ def test_superuser_gets_all_courses(self):
}

self.assertEqual(result_ids, expected_ids) # noqa: PT009

def test_course_listing_with_org_scope(self):
"""
Verify that assigning a course role like course_staff with an org-wide scope
(`course-v1:Org1+*`) grants access to all courses in that org when
the AuthZ course authoring toggle is enabled.
"""
_, _, authz_courses, legacy_courses = self._create_courses()
org_scope = OrgCourseOverviewGlobData(external_key='course-v1:Org1+*')
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
org_scope.external_key,
)

request = self._make_request(self.authorized_user)

with patch.object(
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
"is_enabled",
return_value=True,
):
courses, _ = get_courses_accessible_to_user(request)

result_ids = {c.id for c in courses}

expected_ids = {
*(c.id for c in authz_courses),
*(c.id for c in legacy_courses),
}

self.assertEqual(result_ids, expected_ids) # noqa: PT009

def test_course_listing_with_org_scope_with_toggle(self):
"""
If the authz toggle is enabled only for a subset of org courses, only
those course keys should appear in the resulting course list.
"""
authz_keys, _, _, _ = self._create_courses()
# enable only the first and third course keys
enabled_keys = {str(authz_keys[0]), str(authz_keys[2])}
org_scope = OrgCourseOverviewGlobData(external_key='course-v1:Org1+*')
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
org_scope.external_key,
)

request = self._make_request(self.authorized_user)

with patch.object(
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
"is_enabled",
side_effect=self._mock_authz_toggle(enabled_keys),
):
courses, _ = get_courses_accessible_to_user(request)

result_ids = {c.id for c in courses}

expected = {authz_keys[0], authz_keys[2]}
self.assertEqual(result_ids, expected) # noqa: PT009

def test_course_listing_with_org_scope_without_courses(self):
"""
When the scope is an OrgCourseOverviewGlobData for an org that has no
courses, `get_courses_accessible_to_user` should return an empty
list.
"""
org_scope = OrgCourseOverviewGlobData(external_key='course-v1:Org2+*')
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
org_scope.external_key,
)

request = self._make_request(self.authorized_user)

with patch.object(
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
"is_enabled",
return_value=True,
):
courses, _ = get_courses_accessible_to_user(request)

self.assertEqual(courses, []) # noqa: PT009

def test_course_listing_with_org_scope_fetched_once(self):
"""
Verify that course overviews are fetched once with all authorized orgs.
"""
org_scope1 = OrgCourseOverviewGlobData(external_key='course-v1:Org1+*')
org_scope2 = OrgCourseOverviewGlobData(external_key='course-v1:Org2+*')
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
org_scope1.external_key,
)
assign_role_to_user_in_scope(
self.authorized_user.username,
COURSE_STAFF.external_key,
org_scope2.external_key,
)

request = self._make_request(self.authorized_user)

with patch.object(
core_toggles.AUTHZ_COURSE_AUTHORING_FLAG,
"is_enabled",
return_value=True,
), patch.object(
CourseOverview,
"get_all_courses",
) as mock_get_all_courses:
courses, _ = get_courses_accessible_to_user(request)

mock_get_all_courses.assert_called_once_with(orgs={"Org1", "Org2"})
37 changes: 28 additions & 9 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import BlockUsageLocator
from openedx_authz.api import get_scopes_for_user_and_permission
from openedx_authz.api.data import CourseOverviewData
from openedx_authz.api.data import CourseOverviewData, OrgCourseOverviewGlobData, ScopeData
from openedx_authz.constants.permissions import (
COURSES_MANAGE_COURSE_UPDATES,
COURSES_MANAGE_GROUP_CONFIGURATIONS,
Expand Down Expand Up @@ -817,6 +817,32 @@ def filter_course(course):
return filter(filter_course, filtered_courses)


def _get_course_keys_for_org_scope(org_keys: set[str]):
"""
Convert a set of organization keys into specific course keys.
"""

return CourseOverview.get_all_courses(orgs=org_keys).values_list('id', flat=True)

def _get_course_keys_from_scopes(authz_scopes: list[ScopeData]):
"""
Convert a set of Authz scopes into specific course keys.
"""
course_keys = set()
org_keys = set()
for access in authz_scopes:
if isinstance(access, CourseOverviewData) and access.course_key:
if core_toggles.enable_authz_course_authoring(access.course_key):
course_keys.add(access.course_key)
elif isinstance(access, OrgCourseOverviewGlobData) and access.org:
org_keys.add(access.org)
if org_keys:
course_keys.update(
key for key in _get_course_keys_for_org_scope(org_keys)
if core_toggles.enable_authz_course_authoring(key)
)
return course_keys

def _get_authz_accessible_courses_list(request):
"""
List all courses available to the logged in user by
Expand All @@ -828,14 +854,7 @@ def _get_authz_accessible_courses_list(request):
COURSES_VIEW_COURSE.identifier
)

authz_keys = {
access.course_key
for access in authz_scopes
if isinstance(access, CourseOverviewData) and access.course_key
and core_toggles.enable_authz_course_authoring(access.course_key)
}
return authz_keys

return _get_course_keys_from_scopes(authz_scopes)

def _get_legacy_accessible_courses_list(request):
"""
Expand Down
Loading