feat: AuthZ for course authoring compatibility layer (#38013)

This commit is contained in:
Rodrigo Mendez
2026-03-06 10:35:17 -06:00
committed by GitHub
parent 0c5e96d566
commit 12a46e6463
26 changed files with 628 additions and 174 deletions

View File

@@ -35,6 +35,7 @@ from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -42,6 +43,8 @@ from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, p
TOTAL_COURSES_COUNT = 10 TOTAL_COURSES_COUNT = 10
USER_COURSES_COUNT = 1 USER_COURSES_COUNT = 1
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@ddt.ddt @ddt.ddt
class TestCourseListing(ModuleStoreTestCase): class TestCourseListing(ModuleStoreTestCase):
@@ -303,10 +306,10 @@ class TestCourseListing(ModuleStoreTestCase):
courses_list, __ = _accessible_courses_list_from_groups(self.request) courses_list, __ = _accessible_courses_list_from_groups(self.request)
self.assertEqual(len(courses_list), USER_COURSES_COUNT) self.assertEqual(len(courses_list), USER_COURSES_COUNT)
with self.assertNumQueries(1, table_ignorelist=WAFFLE_TABLES): with self.assertNumQueries(2, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
_accessible_courses_list_from_groups(self.request) _accessible_courses_list_from_groups(self.request)
with self.assertNumQueries(2, table_ignorelist=WAFFLE_TABLES): with self.assertNumQueries(2, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
_accessible_courses_iter_for_tests(self.request) _accessible_courses_iter_for_tests(self.request)
def test_course_listing_errored_deleted_courses(self): def test_course_listing_errored_deleted_courses(self):

View File

@@ -73,24 +73,23 @@ def _user_can_create_library_for_org(user, org=None):
elif user.is_staff: elif user.is_staff:
return True return True
elif settings.FEATURES.get('ENABLE_CREATOR_GROUP', False): elif settings.FEATURES.get('ENABLE_CREATOR_GROUP', False):
org_filter_params = {}
if org:
org_filter_params['org'] = org
is_course_creator = get_course_creator_status(user) == 'granted' is_course_creator = get_course_creator_status(user) == 'granted'
has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).filter(**org_filter_params).exists() if is_course_creator:
has_course_staff_role = ( return True
UserBasedRole(user=user, role=CourseStaffRole.ROLE)
.courses_with_role() has_org_staff_role = OrgStaffRole().has_org_for_user(user, org)
.filter(**org_filter_params) if has_org_staff_role:
.exists() return True
)
has_course_admin_role = ( has_course_staff_role = UserBasedRole(user=user, role=CourseStaffRole.ROLE).has_courses_with_role(org)
UserBasedRole(user=user, role=CourseInstructorRole.ROLE) if has_course_staff_role:
.courses_with_role() return True
.filter(**org_filter_params)
.exists() has_course_admin_role = UserBasedRole(user=user, role=CourseInstructorRole.ROLE).has_courses_with_role(org)
) if has_course_admin_role:
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role return True
return False
else: else:
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present. # EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None) disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)

View File

@@ -14,7 +14,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
) )
from openedx.core.lib.cache_utils import request_cached from openedx.core.lib.cache_utils import request_cached
from common.djangoapps.student.roles import ( from common.djangoapps.student.roles import (
CourseAccessRole, AuthzCompatCourseAccessRole,
CourseBetaTesterRole, CourseBetaTesterRole,
CourseInstructorRole, CourseInstructorRole,
CourseStaffRole, CourseStaffRole,
@@ -66,7 +66,7 @@ def get_role_cache(user: User) -> RoleCache:
@request_cached() @request_cached()
def get_course_roles(user: User) -> list[CourseAccessRole]: def get_course_roles(user: User) -> list[AuthzCompatCourseAccessRole]:
""" """
Returns a list of all course-level roles that this user has. Returns a list of all course-level roles that this user has.

View File

@@ -4,16 +4,23 @@ adding users, removing users, and listing members
""" """
from collections import defaultdict
import logging import logging
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from common.djangoapps.student.signals.signals import emit_course_access_role_added, emit_course_access_role_removed
from opaque_keys.edx.django.models import CourseKeyField from opaque_keys.edx.django.models import CourseKeyField
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import CourseLocator
from openedx_authz.api import users as authz_api
from openedx_authz.constants import roles as authz_roles
from openedx.core.lib.cache_utils import get_cache
from common.djangoapps.student.models import CourseAccessRole from common.djangoapps.student.models import CourseAccessRole
from openedx.core.lib.cache_utils import get_cache
from openedx.core.toggles import enable_authz_course_authoring
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -27,6 +34,46 @@ ACCESS_ROLES_INHERITANCE = {}
ROLE_CACHE_UNGROUPED_ROLES__KEY = 'ungrouped' ROLE_CACHE_UNGROUPED_ROLES__KEY = 'ungrouped'
def get_authz_role_from_legacy_role(legacy_role: str) -> str:
return authz_roles.LEGACY_COURSE_ROLE_EQUIVALENCES.get(legacy_role, None)
def get_legacy_role_from_authz_role(authz_role: str) -> str:
return next((k for k, v in authz_roles.LEGACY_COURSE_ROLE_EQUIVALENCES.items() if v == authz_role), None)
def authz_add_role(user: User, authz_role: str, course_key: str):
"""
Add a user's role in a course if not already added.
Args:
user (User): The user whose role is being changed.
authz_role (str): The new authorization role to assign (authz role, not legacy).
course_key (str): The course key where the role change is taking effect.
"""
course_locator = CourseLocator.from_string(course_key)
# Check if the user is not already assigned this role for this course
existing_assignments = authz_api.get_user_role_assignments_in_scope(
user_external_key=user.username,
scope_external_key=course_key
)
existing_roles = [existing_role.external_key
for existing_assignment in existing_assignments
for existing_role in existing_assignment.roles]
if authz_role in existing_roles:
return
# Assign new role
authz_api.assign_role_to_user_in_scope(
user_external_key=user.username,
role_external_key=authz_role,
scope_external_key=course_key
)
legacy_role = get_legacy_role_from_authz_role(authz_role)
emit_course_access_role_added(user, course_locator, course_locator.org, legacy_role)
def register_access_role(cls): def register_access_role(cls):
""" """
Decorator that allows access roles to be registered within the roles module and referenced by their Decorator that allows access roles to be registered within the roles module and referenced by their
@@ -70,6 +117,43 @@ def get_role_cache_key_for_course(course_key=None):
return str(course_key) if course_key else ROLE_CACHE_UNGROUPED_ROLES__KEY return str(course_key) if course_key else ROLE_CACHE_UNGROUPED_ROLES__KEY
@dataclass(frozen=True)
class AuthzCompatCourseAccessRole:
"""
Generic data class for storing CourseAccessRole-compatible data
to be used inside BulkRoleCache and RoleCache.
This allows the cache to store both legacy and openedx-authz compatible roles
"""
user_id: int
username: str
org: str
course_id: str # Course key
role: str
def get_authz_compat_course_access_roles_for_user(user: User) -> set[AuthzCompatCourseAccessRole]:
"""
Retrieve all CourseAccessRole objects for a given user and convert them to AuthzCompatCourseAccessRole objects.
"""
compat_role_assignments = set()
assignments = authz_api.get_user_role_assignments(user_external_key=user.username)
for assignment in assignments:
for role in assignment.roles:
legacy_role = get_legacy_role_from_authz_role(authz_role=role.external_key)
course_key = assignment.scope.external_key
parsed_key = CourseKey.from_string(course_key)
org = parsed_key.org
compat_role = AuthzCompatCourseAccessRole(
user_id=user.id,
username=user.username,
org=org,
course_id=course_key,
role=legacy_role
)
compat_role_assignments.add(compat_role)
return compat_role_assignments
class BulkRoleCache: # lint-amnesty, pylint: disable=missing-class-docstring class BulkRoleCache: # lint-amnesty, pylint: disable=missing-class-docstring
""" """
This class provides a caching mechanism for roles grouped by users and courses, This class provides a caching mechanism for roles grouped by users and courses,
@@ -98,13 +182,29 @@ class BulkRoleCache: # lint-amnesty, pylint: disable=missing-class-docstring
roles_by_user = defaultdict(lambda: defaultdict(set)) roles_by_user = defaultdict(lambda: defaultdict(set))
get_cache(cls.CACHE_NAMESPACE)[cls.CACHE_KEY] = roles_by_user get_cache(cls.CACHE_NAMESPACE)[cls.CACHE_KEY] = roles_by_user
# Legacy roles
for role in CourseAccessRole.objects.filter(user__in=users).select_related('user'): for role in CourseAccessRole.objects.filter(user__in=users).select_related('user'):
user_id = role.user.id user_id = role.user.id
course_id = get_role_cache_key_for_course(role.course_id) course_id = get_role_cache_key_for_course(role.course_id)
# Add role to the set in roles_by_user[user_id][course_id] # Add role to the set in roles_by_user[user_id][course_id]
user_roles_set_for_course = roles_by_user[user_id][course_id] user_roles_set_for_course = roles_by_user[user_id][course_id]
user_roles_set_for_course.add(role) compat_role = AuthzCompatCourseAccessRole(
user_id=role.user.id,
username=role.user.username,
org=role.org,
course_id=role.course_id,
role=role.role
)
user_roles_set_for_course.add(compat_role)
# openedx-authz roles
for user in users:
compat_roles = get_authz_compat_course_access_roles_for_user(user)
for role in compat_roles:
course_id = get_role_cache_key_for_course(role.course_id)
user_roles_set_for_course = roles_by_user[user.id][course_id]
user_roles_set_for_course.add(compat_role)
users_without_roles = [u for u in users if u.id not in roles_by_user] users_without_roles = [u for u in users if u.id not in roles_by_user]
for user in users_without_roles: for user in users_without_roles:
@@ -117,7 +217,7 @@ class BulkRoleCache: # lint-amnesty, pylint: disable=missing-class-docstring
class RoleCache: class RoleCache:
""" """
A cache of the CourseAccessRoles held by a particular user. A cache of the AuthzCompatCourseAccessRoles held by a particular user.
Internal data structures should be accessed by getter and setter methods; Internal data structures should be accessed by getter and setter methods;
don't use `_roles_by_course_id` or `_roles` directly. don't use `_roles_by_course_id` or `_roles` directly.
_roles_by_course_id: This is the data structure as saved in the RequestCache. _roles_by_course_id: This is the data structure as saved in the RequestCache.
@@ -134,18 +234,35 @@ class RoleCache:
self._roles_by_course_id = BulkRoleCache.get_user_roles(user) self._roles_by_course_id = BulkRoleCache.get_user_roles(user)
except KeyError: except KeyError:
self._roles_by_course_id = {} self._roles_by_course_id = {}
# openedx-authz compatibility implementation
compat_roles = get_authz_compat_course_access_roles_for_user(user)
for compat_role in compat_roles:
course_id = get_role_cache_key_for_course(compat_role.course_id)
if not self._roles_by_course_id.get(course_id):
self._roles_by_course_id[course_id] = set()
self._roles_by_course_id[course_id].add(compat_role)
# legacy implementation
roles = CourseAccessRole.objects.filter(user=user).all() roles = CourseAccessRole.objects.filter(user=user).all()
for role in roles: for role in roles:
course_id = get_role_cache_key_for_course(role.course_id) course_id = get_role_cache_key_for_course(role.course_id)
if not self._roles_by_course_id.get(course_id): if not self._roles_by_course_id.get(course_id):
self._roles_by_course_id[course_id] = set() self._roles_by_course_id[course_id] = set()
self._roles_by_course_id[course_id].add(role) compat_role = AuthzCompatCourseAccessRole(
user_id=user.id,
username=user.username,
org=role.org,
course_id=role.course_id,
role=role.role
)
self._roles_by_course_id[course_id].add(compat_role)
self._roles = set() self._roles = set()
for roles_for_course in self._roles_by_course_id.values(): for roles_for_course in self._roles_by_course_id.values():
self._roles.update(roles_for_course) self._roles.update(roles_for_course)
@staticmethod @staticmethod
def get_roles(role): def get_roles(role: str) -> set[str]:
""" """
Return the roles that should have the same permissions as the specified role. Return the roles that should have the same permissions as the specified role.
""" """
@@ -269,13 +386,33 @@ class RoleBase(AccessRole):
return user._roles.has_role(self._role_name, self.course_key, self.org) return user._roles.has_role(self._role_name, self.course_key, self.org)
def add_users(self, *users): def _authz_add_users(self, users):
""" """
Add the supplied django users to this role. Add the supplied django users to this role.
AuthZ compatibility layer
"""
role = get_authz_role_from_legacy_role(self.ROLE)
# silently ignores anonymous and inactive users so that any that are
# legit get updated.
for user in users:
if user.is_authenticated and user.is_active:
authz_add_role(
user=user,
authz_role=role,
course_key=str(self.course_key),
)
if hasattr(user, '_roles'):
del user._roles
def _legacy_add_users(self, users):
"""
Add the supplied django users to this role.
legacy implementation
""" """
# silently ignores anonymous and inactive users so that any that are # silently ignores anonymous and inactive users so that any that are
# legit get updated. # legit get updated.
from common.djangoapps.student.models import CourseAccessRole # lint-amnesty, pylint: disable=redefined-outer-name, reimported from common.djangoapps.student.models import \
CourseAccessRole # lint-amnesty, pylint: disable=redefined-outer-name, reimported
for user in users: for user in users:
if user.is_authenticated and user.is_active: if user.is_authenticated and user.is_active:
CourseAccessRole.objects.get_or_create( CourseAccessRole.objects.get_or_create(
@@ -284,9 +421,38 @@ class RoleBase(AccessRole):
if hasattr(user, '_roles'): if hasattr(user, '_roles'):
del user._roles del user._roles
def remove_users(self, *users): def add_users(self, *users):
"""
Add the supplied django users to this role.
"""
if enable_authz_course_authoring(self.course_key):
self._authz_add_users(users)
else:
self._legacy_add_users(users)
def _authz_remove_users(self, users):
""" """
Remove the supplied django users from this role. Remove the supplied django users from this role.
AuthZ compatibility layer
"""
usernames = [user.username for user in users]
role = get_authz_role_from_legacy_role(self.ROLE)
course_key_str = str(self.course_key)
course_locator = CourseLocator.from_string(course_key_str)
authz_api.batch_unassign_role_from_users(
users=usernames,
role_external_key=role,
scope_external_key=course_key_str
)
for user in users:
emit_course_access_role_removed(user, course_locator, course_locator.org, self.ROLE)
if hasattr(user, '_roles'):
del user._roles
def _legacy_remove_users(self, users):
"""
Remove the supplied django users from this role.
legacy implementation
""" """
entries = CourseAccessRole.objects.filter( entries = CourseAccessRole.objects.filter(
user__in=users, role=self._role_name, org=self.org, course_id=self.course_key user__in=users, role=self._role_name, org=self.org, course_id=self.course_key
@@ -296,9 +462,33 @@ class RoleBase(AccessRole):
if hasattr(user, '_roles'): if hasattr(user, '_roles'):
del user._roles del user._roles
def users_with_role(self): def remove_users(self, *users):
"""
Remove the supplied django users from this role.
"""
if enable_authz_course_authoring(self.course_key):
self._authz_remove_users(users)
else:
self._legacy_remove_users(users)
def _authz_users_with_role(self):
""" """
Return a django QuerySet for all of the users with this role Return a django QuerySet for all of the users with this role
AuthZ compatibility layer
"""
role = get_authz_role_from_legacy_role(self.ROLE)
users_data = authz_api.get_users_for_role_in_scope(
role_external_key=role,
scope_external_key=str(self.course_key)
)
usernames = [user_data.username for user_data in users_data]
entries = User.objects.filter(username__in=usernames)
return entries
def _legacy_users_with_role(self):
"""
Return a django QuerySet for all of the users with this role
legacy implementation
""" """
# Org roles don't query by CourseKey, so use CourseKeyField.Empty for that query # Org roles don't query by CourseKey, so use CourseKeyField.Empty for that query
if self.course_key is None: if self.course_key is None:
@@ -310,12 +500,63 @@ class RoleBase(AccessRole):
) )
return entries return entries
def users_with_role(self):
"""
Return a django QuerySet for all of the users with this role
"""
if enable_authz_course_authoring(self.course_key):
return self._authz_users_with_role()
else:
return self._legacy_users_with_role()
def _authz_get_orgs_for_user(self, user) -> list[str]:
"""
Returns a list of org short names for the user with given role.
AuthZ compatibility layer
"""
# TODO: This will be implemented on Milestone 1
# of the Authz for Course Authoring project
return []
def _legacy_get_orgs_for_user(self, user) -> list[str]:
"""
Returns a list of org short names for the user with given role.
legacy implementation
"""
return list(CourseAccessRole.objects.filter(user=user, role=self._role_name).values_list('org', flat=True))
def get_orgs_for_user(self, user): def get_orgs_for_user(self, user):
""" """
Returns a list of org short names for the user with given role. Returns a list of org short names for the user with given role.
""" """
return CourseAccessRole.objects.filter(user=user, role=self._role_name).values_list('org', flat=True) if enable_authz_course_authoring(self.course_key):
return self._authz_get_orgs_for_user(user)
else:
return self._legacy_get_orgs_for_user(user)
def has_org_for_user(self, user: User, org: str | None = None) -> bool:
"""
Checks whether a user has a specific role within an org.
Arguments:
user: user to check against access to role
org: optional org to check against access to role,
if not specified, will return True if the user has access to at least one org
"""
if enable_authz_course_authoring(self.course_key):
orgs_with_role = self.get_orgs_for_user(user)
if org:
return org in orgs_with_role
return len(orgs_with_role) > 0
else:
# Use ORM query directly for performance
filter_params = {
'user': user,
'role': self._role_name
}
if org:
filter_params['org'] = org
return CourseAccessRole.objects.filter(**filter_params).exists()
class CourseRole(RoleBase): class CourseRole(RoleBase):
""" """
@@ -329,9 +570,25 @@ class CourseRole(RoleBase):
super().__init__(role, course_key.org, course_key) super().__init__(role, course_key.org, course_key)
@classmethod @classmethod
def course_group_already_exists(self, course_key): # lint-amnesty, pylint: disable=bad-classmethod-argument def _authz_course_group_already_exists(cls, course_key): # lint-amnesty, pylint: disable=bad-classmethod-argument
# AuthZ compatibility layer
return len(authz_api.get_all_user_role_assignments_in_scope(scope_external_key=str(course_key))) > 0
@classmethod
def _legacy_course_group_already_exists(cls, course_key): # lint-amnesty, pylint: disable=bad-classmethod-argument
# Legacy implementation
return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists() return CourseAccessRole.objects.filter(org=course_key.org, course_id=course_key).exists()
@classmethod
def course_group_already_exists(cls, course_key): # lint-amnesty, pylint: disable=bad-classmethod-argument
"""
Returns whether role assignations for a course already exist
"""
if enable_authz_course_authoring(course_key):
return cls._authz_course_group_already_exists(course_key)
else:
return cls._legacy_course_group_already_exists(course_key)
def __repr__(self): def __repr__(self):
return f'<{self.__class__.__name__}: course_key={self.course_key}>' return f'<{self.__class__.__name__}: course_key={self.course_key}>'
@@ -519,9 +776,18 @@ class UserBasedRole:
Grant this object's user the object's role for the supplied courses Grant this object's user the object's role for the supplied courses
""" """
if self.user.is_authenticated and self.user.is_active: if self.user.is_authenticated and self.user.is_active:
authz_role = get_authz_role_from_legacy_role(self.role)
for course_key in course_keys: for course_key in course_keys:
entry = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org) if enable_authz_course_authoring(course_key):
entry.save() # AuthZ compatibility layer
authz_add_role(
user=self.user,
authz_role=authz_role,
course_key=str(course_key),
)
else:
entry = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org)
entry.save()
if hasattr(self.user, '_roles'): if hasattr(self.user, '_roles'):
del self.user._roles del self.user._roles
else: else:
@@ -531,18 +797,102 @@ class UserBasedRole:
""" """
Remove the supplied courses from this user's configured role. Remove the supplied courses from this user's configured role.
""" """
# CourseAccessRoles for courses managed by AuthZ should already be removed, so always doing this is ok
entries = CourseAccessRole.objects.filter(user=self.user, role=self.role, course_id__in=course_keys) entries = CourseAccessRole.objects.filter(user=self.user, role=self.role, course_id__in=course_keys)
entries.delete() entries.delete()
# Execute bulk delete on AuthZ
role = get_authz_role_from_legacy_role(self.role)
for course_key in course_keys:
course_key_str = str(course_key)
success = authz_api.unassign_role_from_user(
user_external_key=self.user.username,
role_external_key=role,
scope_external_key=course_key_str
)
if success:
course_locator = CourseLocator.from_string(course_key_str)
emit_course_access_role_removed(self.user, course_locator, course_locator.org, self.role)
if hasattr(self.user, '_roles'): if hasattr(self.user, '_roles'):
del self.user._roles del self.user._roles
def courses_with_role(self): def courses_with_role(self) -> set[AuthzCompatCourseAccessRole]:
""" """
Return a django QuerySet for all of the courses with this user x (or derived from x) role. You can access Return a set of AuthzCompatCourseAccessRole for all of the courses with this user x (or derived from x) role.
any of these properties on each result record:
* user (will be self.user--thus uninteresting)
* org
* course_id
* role (will be self.role--thus uninteresting)
""" """
return CourseAccessRole.objects.filter(role__in=RoleCache.get_roles(self.role), user=self.user) roles = RoleCache.get_roles(self.role)
legacy_assignments = CourseAccessRole.objects.filter(role__in=roles, user=self.user)
# Get all assignments for a user to a role
new_authz_roles = [get_authz_role_from_legacy_role(role) for role in roles]
all_authz_user_assignments = authz_api.get_user_role_assignments(
user_external_key=self.user.username
)
all_assignments = set()
for legacy_assignment in legacy_assignments:
for role in roles:
all_assignments.add(AuthzCompatCourseAccessRole(
user_id=self.user.id,
username=self.user.username,
org=legacy_assignment.org,
course_id=legacy_assignment.course_id,
role=role
))
for assignment in all_authz_user_assignments:
for role in assignment.roles:
if role.external_key not in new_authz_roles:
continue
legacy_role = get_legacy_role_from_authz_role(authz_role=role.external_key)
course_key = assignment.scope.external_key
parsed_key = CourseKey.from_string(course_key)
org = parsed_key.org
all_assignments.add(AuthzCompatCourseAccessRole(
user_id=self.user.id,
username=self.user.username,
org=org,
course_id=course_key,
role=legacy_role
))
return all_assignments
def has_courses_with_role(self, org: str | None = None) -> bool:
"""
Return whether this user has any courses with this role and optional org (or derived roles)
Arguments:
org (str): Optional org to filter by
"""
roles = RoleCache.get_roles(self.role)
# First check if we have any legacy assignment with an optimized ORM query
filter_params = {
'user': self.user,
'role__in': roles
}
if org:
filter_params['org'] = org
has_legacy_assignments = CourseAccessRole.objects.filter(**filter_params).exists()
if has_legacy_assignments:
return True
# Then check for authz assignments
new_authz_roles = [get_authz_role_from_legacy_role(role) for role in roles]
all_authz_user_assignments = authz_api.get_user_role_assignments(
user_external_key=self.user.username
)
for assignment in all_authz_user_assignments:
for role in assignment.roles:
if role.external_key not in new_authz_roles:
continue
if org is None:
# There is at least one assignment, short circuit
return True
course_key = assignment.scope.external_key
parsed_key = CourseKey.from_string(course_key)
if org == parsed_key.org:
return True
return False

View File

@@ -6,12 +6,17 @@ Tests of student.roles
import ddt import ddt
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.test import TestCase from django.test import TestCase
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator from opaque_keys.edx.locator import LibraryLocator
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx_authz.engine.enforcer import AuthzEnforcer
from common.djangoapps.student.admin import CourseAccessRoleHistoryAdmin from common.djangoapps.student.admin import CourseAccessRoleHistoryAdmin
from common.djangoapps.student.models import CourseAccessRoleHistory, User from common.djangoapps.student.models import CourseAccessRoleHistory, User
from common.djangoapps.student.roles import ( from common.djangoapps.student.roles import (
AuthzCompatCourseAccessRole,
CourseAccessRole, CourseAccessRole,
CourseBetaTesterRole, CourseBetaTesterRole,
CourseInstructorRole, CourseInstructorRole,
@@ -32,8 +37,10 @@ from common.djangoapps.student.roles import (
) )
from common.djangoapps.student.role_helpers import get_course_roles, has_staff_roles from common.djangoapps.student.role_helpers import get_course_roles, has_staff_roles
from common.djangoapps.student.tests.factories import AnonymousUserFactory, InstructorFactory, StaffFactory, UserFactory from common.djangoapps.student.tests.factories import AnonymousUserFactory, InstructorFactory, StaffFactory, UserFactory
from openedx.core.toggles import AUTHZ_COURSE_AUTHORING_FLAG
@ddt.ddt
class RolesTestCase(TestCase): class RolesTestCase(TestCase):
""" """
Tests of student.roles Tests of student.roles
@@ -41,8 +48,10 @@ class RolesTestCase(TestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self._seed_database_with_policies()
self.course_key = CourseKey.from_string('course-v1:course-v1:edX+toy+2012_Fall') self.course_key = CourseKey.from_string('course-v1:course-v1:edX+toy+2012_Fall')
self.course_loc = self.course_key.make_usage_key('course', '2012_Fall') self.course_loc = self.course_key.make_usage_key('course', '2012_Fall')
self.course = CourseOverviewFactory.create(id=self.course_key)
self.anonymous_user = AnonymousUserFactory() self.anonymous_user = AnonymousUserFactory()
self.student = UserFactory() self.student = UserFactory()
self.global_staff = UserFactory(is_staff=True) self.global_staff = UserFactory(is_staff=True)
@@ -50,37 +59,67 @@ class RolesTestCase(TestCase):
self.course_instructor = InstructorFactory(course_key=self.course_key) self.course_instructor = InstructorFactory(course_key=self.course_key)
self.orgs = ["Marvel", "DC"] self.orgs = ["Marvel", "DC"]
def test_global_staff(self): @classmethod
assert not GlobalStaff().has_user(self.student) def _seed_database_with_policies(cls):
assert not GlobalStaff().has_user(self.course_staff) """Seed the database with policies from the policy file for openedx_authz tests.
assert not GlobalStaff().has_user(self.course_instructor)
assert GlobalStaff().has_user(self.global_staff)
def test_has_staff_roles(self): This simulates the one-time database seeding that would happen
assert has_staff_roles(self.global_staff, self.course_key) during application deployment, separate from the runtime policy loading.
assert has_staff_roles(self.course_staff, self.course_key) """
assert has_staff_roles(self.course_instructor, self.course_key) import pkg_resources
assert not has_staff_roles(self.student, self.course_key) from openedx_authz.engine.utils import migrate_policy_between_enforcers
import casbin
def test_get_course_roles(self): global_enforcer = AuthzEnforcer.get_enforcer()
assert not list(get_course_roles(self.student)) global_enforcer.load_policy()
assert not list(get_course_roles(self.global_staff)) model_path = pkg_resources.resource_filename("openedx_authz.engine", "config/model.conf")
assert list(get_course_roles(self.course_staff)) == [ policy_path = pkg_resources.resource_filename("openedx_authz.engine", "config/authz.policy")
CourseAccessRole(
user=self.course_staff, migrate_policy_between_enforcers(
course_id=self.course_key, source_enforcer=casbin.Enforcer(model_path, policy_path),
org=self.course_key.org, target_enforcer=global_enforcer,
role=CourseStaffRole.ROLE, )
) global_enforcer.clear_policy() # Clear to simulate fresh start for each test
]
assert list(get_course_roles(self.course_instructor)) == [ @ddt.data(True, False)
CourseAccessRole( def test_global_staff(self, authz_enabled):
user=self.course_instructor, with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
course_id=self.course_key, assert not GlobalStaff().has_user(self.student)
org=self.course_key.org, assert not GlobalStaff().has_user(self.course_staff)
role=CourseInstructorRole.ROLE, assert not GlobalStaff().has_user(self.course_instructor)
) assert GlobalStaff().has_user(self.global_staff)
]
@ddt.data(True, False)
def test_has_staff_roles(self, authz_enabled):
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
assert has_staff_roles(self.global_staff, self.course_key)
assert has_staff_roles(self.course_staff, self.course_key)
assert has_staff_roles(self.course_instructor, self.course_key)
assert not has_staff_roles(self.student, self.course_key)
@ddt.data(True, False)
def test_get_course_roles(self, authz_enabled):
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
assert not list(get_course_roles(self.student))
assert not list(get_course_roles(self.global_staff))
assert list(get_course_roles(self.course_staff)) == [
AuthzCompatCourseAccessRole(
user_id=self.course_staff.id,
username=self.course_staff.username,
course_id=self.course_key,
org=self.course_key.org,
role=CourseStaffRole.ROLE,
)
]
assert list(get_course_roles(self.course_instructor)) == [
AuthzCompatCourseAccessRole(
user_id=self.course_instructor.id,
username=self.course_instructor.username,
course_id=self.course_key,
org=self.course_key.org,
role=CourseInstructorRole.ROLE,
)
]
def test_group_name_case_sensitive(self): def test_group_name_case_sensitive(self):
uppercase_course_id = "ORG/COURSE/NAME" uppercase_course_id = "ORG/COURSE/NAME"
@@ -100,20 +139,22 @@ class RolesTestCase(TestCase):
assert not CourseRole(role, lowercase_course_key).has_user(uppercase_user) assert not CourseRole(role, lowercase_course_key).has_user(uppercase_user)
assert CourseRole(role, uppercase_course_key).has_user(uppercase_user) assert CourseRole(role, uppercase_course_key).has_user(uppercase_user)
def test_course_role(self): @ddt.data(True, False)
def test_course_role(self, authz_enabled):
""" """
Test that giving a user a course role enables access appropriately Test that giving a user a course role enables access appropriately
""" """
assert not CourseStaffRole(self.course_key).has_user(self.student), \ with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
f'Student has premature access to {self.course_key}' assert not CourseStaffRole(self.course_key).has_user(self.student), \
CourseStaffRole(self.course_key).add_users(self.student) f'Student has premature access to {self.course_key}'
assert CourseStaffRole(self.course_key).has_user(self.student), \ CourseStaffRole(self.course_key).add_users(self.student)
f"Student doesn't have access to {str(self.course_key)}" assert CourseStaffRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {str(self.course_key)}"
# remove access and confirm # remove access and confirm
CourseStaffRole(self.course_key).remove_users(self.student) CourseStaffRole(self.course_key).remove_users(self.student)
assert not CourseStaffRole(self.course_key).has_user(self.student), \ assert not CourseStaffRole(self.course_key).has_user(self.student), \
f'Student still has access to {self.course_key}' f'Student still has access to {self.course_key}'
def test_org_role(self): def test_org_role(self):
""" """
@@ -158,26 +199,30 @@ class RolesTestCase(TestCase):
assert not CourseInstructorRole(self.course_key).has_user(self.student), \ assert not CourseInstructorRole(self.course_key).has_user(self.student), \
f"Student doesn't have access to {str(self.course_key)}" f"Student doesn't have access to {str(self.course_key)}"
def test_get_user_for_role(self): @ddt.data(True, False)
def test_get_user_for_role(self, authz_enabled):
""" """
test users_for_role test users_for_role
""" """
role = CourseStaffRole(self.course_key) with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
role.add_users(self.student) role = CourseStaffRole(self.course_key)
assert len(role.users_with_role()) > 0 role.add_users(self.student)
assert len(role.users_with_role()) > 0
def test_add_users_doesnt_add_duplicate_entry(self): @ddt.data(True, False)
def test_add_users_doesnt_add_duplicate_entry(self, authz_enabled):
""" """
Tests that calling add_users multiple times before a single call Tests that calling add_users multiple times before a single call
to remove_users does not result in the user remaining in the group. to remove_users does not result in the user remaining in the group.
""" """
role = CourseStaffRole(self.course_key) with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
role.add_users(self.student) role = CourseStaffRole(self.course_key)
assert role.has_user(self.student) role.add_users(self.student)
# Call add_users a second time, then remove just once. assert role.has_user(self.student)
role.add_users(self.student) # Call add_users a second time, then remove just once.
role.remove_users(self.student) role.add_users(self.student)
assert not role.has_user(self.student) role.remove_users(self.student)
assert not role.has_user(self.student)
def test_get_orgs_for_user(self): def test_get_orgs_for_user(self):
""" """

View File

@@ -8,6 +8,7 @@ from datetime import datetime
from unittest import mock from unittest import mock
import ddt import ddt
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest import pytest
from ccx_keys.locator import CCXLocator from ccx_keys.locator import CCXLocator
from django.conf import settings from django.conf import settings
@@ -34,7 +35,7 @@ from openedx.core.djangoapps.content.block_structure.api import get_course_in_ca
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.models import ContentTypeGatingConfig
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@mock.patch.dict( @mock.patch.dict(
@@ -234,7 +235,7 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
# TODO: decrease query count as part of REVO-28 # TODO: decrease query count as part of REVO-28
QUERY_COUNT = 34 QUERY_COUNT = 36
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (QUERY_COUNT, 2), ('no_overrides', 1, True, False): (QUERY_COUNT, 2),

View File

@@ -10,6 +10,7 @@ from django.test.client import RequestFactory
from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import SampleCourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import SampleCourseFactory, check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
@@ -17,6 +18,8 @@ from xmodule.modulestore.tests.sample_courses import BlockInfo # lint-amnesty,
from ..api import get_blocks from ..api import get_blocks
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
class TestGetBlocks(SharedModuleStoreTestCase): class TestGetBlocks(SharedModuleStoreTestCase):
""" """
@@ -196,7 +199,7 @@ class TestGetBlocksQueryCountsBase(SharedModuleStoreTestCase):
get_blocks on the given course. get_blocks on the given course.
""" """
with check_mongo_calls(expected_mongo_queries): with check_mongo_calls(expected_mongo_queries):
with self.assertNumQueries(expected_sql_queries): with self.assertNumQueries(expected_sql_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
get_blocks(self.request, course.location, self.user) get_blocks(self.request, course.location, self.user)
@@ -212,11 +215,11 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase):
self._get_blocks( self._get_blocks(
course, course,
expected_mongo_queries=0, expected_mongo_queries=0,
expected_sql_queries=14, expected_sql_queries=16,
) )
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.split, 2, 24), (ModuleStoreEnum.Type.split, 2, 26),
) )
@ddt.unpack @ddt.unpack
def test_query_counts_uncached(self, store_type, expected_mongo_queries, num_sql_queries): def test_query_counts_uncached(self, store_type, expected_mongo_queries, num_sql_queries):

View File

@@ -16,10 +16,14 @@ from lms.djangoapps.gating import api as lms_gating_api
import openedx.core.djangoapps.content.block_structure.api as bs_api import openedx.core.djangoapps.content.block_structure.api as bs_api
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
from openedx.core.djangoapps.course_apps.toggles import EXAMS_IDA from openedx.core.djangoapps.course_apps.toggles import EXAMS_IDA
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.core.lib.gating import api as gating_api from openedx.core.lib.gating import api as gating_api
from ..milestones import MilestonesAndSpecialExamsTransformer from ..milestones import MilestonesAndSpecialExamsTransformer
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@ddt.ddt @ddt.ddt
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True}) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True})
@@ -171,7 +175,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
# get data back. This would happen as a part of publishing in a production system. # get data back. This would happen as a part of publishing in a production system.
bs_api.update_course_in_cache(self.course.id) bs_api.update_course_in_cache(self.course.id)
with self.assertNumQueries(4): with self.assertNumQueries(6, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion) self.get_blocks_and_check_against_expected(self.user, expected_blocks_before_completion)
# clear the request cache to simulate a new request # clear the request cache to simulate a new request
@@ -184,7 +188,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
self.user, self.user,
) )
with self.assertNumQueries(4): with self.assertNumQueries(4, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL) self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
def test_staff_access(self): def test_staff_access(self):

View File

@@ -7,6 +7,7 @@ import datetime
import itertools import itertools
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest import pytest
import ddt import ddt
import pytz import pytz
@@ -70,7 +71,7 @@ from openedx.features.enterprise_support.tests.factories import (
) )
from crum import set_current_request from crum import set_current_request
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
# pylint: disable=protected-access # pylint: disable=protected-access
@@ -878,16 +879,16 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
if user_attr_name == 'user_staff' and action == 'see_exists': if user_attr_name == 'user_staff' and action == 'see_exists':
# always checks staff role, and if the course has started, check the duration configuration # always checks staff role, and if the course has started, check the duration configuration
if course_attr_name == 'course_started':
num_queries = 2
else:
num_queries = 1
elif user_attr_name == 'user_normal' and action == 'see_exists':
if course_attr_name == 'course_started': if course_attr_name == 'course_started':
num_queries = 4 num_queries = 4
else:
num_queries = 3
elif user_attr_name == 'user_normal' and action == 'see_exists':
if course_attr_name == 'course_started':
num_queries = 6
else: else:
# checks staff role and enrollment data # checks staff role and enrollment data
num_queries = 2 num_queries = 4
elif user_attr_name == 'user_anonymous' and action == 'see_exists': elif user_attr_name == 'user_anonymous' and action == 'see_exists':
if course_attr_name == 'course_started': if course_attr_name == 'course_started':
num_queries = 1 num_queries = 1
@@ -896,7 +897,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
else: else:
# if the course has started, check the duration configuration # if the course has started, check the duration configuration
if action == 'see_exists' and course_attr_name == 'course_started': if action == 'see_exists' and course_attr_name == 'course_started':
num_queries = 3 num_queries = 5
else: else:
num_queries = 0 num_queries = 0
@@ -950,17 +951,17 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
if user_attr_name == 'user_staff': if user_attr_name == 'user_staff':
if course_attr_name == 'course_started': if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role # read: CourseAccessRole + django_comment_client.Role
num_queries = 2 num_queries = 4
else: else:
# read: CourseAccessRole + EnterpriseCourseEnrollment # read: CourseAccessRole + EnterpriseCourseEnrollment
num_queries = 2 num_queries = 4
elif user_attr_name == 'user_normal': elif user_attr_name == 'user_normal':
if course_attr_name == 'course_started': if course_attr_name == 'course_started':
# read: CourseAccessRole + django_comment_client.Role + FBEEnrollmentExclusion + CourseMode # read: CourseAccessRole + django_comment_client.Role + FBEEnrollmentExclusion + CourseMode
num_queries = 4 num_queries = 6
else: else:
# read: CourseAccessRole + CourseEnrollmentAllowed + EnterpriseCourseEnrollment # read: CourseAccessRole + CourseEnrollmentAllowed + EnterpriseCourseEnrollment
num_queries = 3 num_queries = 5
elif user_attr_name == 'user_anonymous': elif user_attr_name == 'user_anonymous':
if course_attr_name == 'course_started': if course_attr_name == 'course_started':
# read: CourseMode # read: CourseMode

View File

@@ -4,6 +4,7 @@ Test for lms courseware app, module data (runtime data storage for XBlocks)
import json import json
from functools import partial from functools import partial
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, FilteredQueryCountMixin
import pytest import pytest
from django.db import connections, DatabaseError from django.db import connections, DatabaseError
@@ -13,6 +14,7 @@ from xblock.exceptions import KeyValueMultiSaveError
from xblock.fields import BlockScope, Scope, ScopeIds from xblock.fields import BlockScope, Scope, ScopeIds
from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache, InvalidScopeError from lms.djangoapps.courseware.model_data import DjangoKeyValueStore, FieldDataCache, InvalidScopeError
from lms.djangoapps.courseware.models import ( from lms.djangoapps.courseware.models import (
StudentModule, StudentModule,
@@ -27,6 +29,8 @@ from lms.djangoapps.courseware.tests.factories import StudentModuleFactory as cm
from lms.djangoapps.courseware.tests.factories import StudentPrefsFactory from lms.djangoapps.courseware.tests.factories import StudentPrefsFactory
from lms.djangoapps.courseware.tests.factories import UserStateSummaryFactory from lms.djangoapps.courseware.tests.factories import UserStateSummaryFactory
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
def mock_field(scope, name): def mock_field(scope, name):
field = Mock() field = Mock()
@@ -239,7 +243,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
assert exception_context.value.saved_field_names == [] assert exception_context.value.saved_field_names == []
class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring class TestMissingStudentModule(FilteredQueryCountMixin, TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
# Tell Django to clean out all databases, not just default # Tell Django to clean out all databases, not just default
databases = set(connections) databases = set(connections)
@@ -276,7 +280,9 @@ class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missi
# on the StudentModule). # on the StudentModule).
# Django 1.8 also has a number of other BEGIN and SAVESTATE queries. # Django 1.8 also has a number of other BEGIN and SAVESTATE queries.
with self.assertNumQueries(4, using='default'): with self.assertNumQueries(4, using='default'):
with self.assertNumQueries(2, using='student_module_history'): with self.assertNumQueries(2,
using='student_module_history',
table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.kvs.set(user_state_key('a_field'), 'a_value') self.kvs.set(user_state_key('a_field'), 'a_value')
assert 1 == sum(len(cache) for cache in self.field_data_cache.cache.values()) assert 1 == sum(len(cache) for cache in self.field_data_cache.cache.values())

View File

@@ -89,7 +89,7 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
from openedx.core.djangoapps.credit.api import set_credit_requirements from openedx.core.djangoapps.credit.api import set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import get_mock_request from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, get_mock_request
from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE from openedx.core.djangoapps.video_config.toggles import PUBLIC_VIDEO_SHARE
from openedx.core.lib.url_utils import quote_slashes from openedx.core.lib.url_utils import quote_slashes
from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.models import ContentTypeGatingConfig
@@ -108,7 +108,7 @@ from openedx.features.enterprise_support.tests.mixins.enterprise import Enterpri
from openedx.features.enterprise_support.api import add_enterprise_customer_to_session from openedx.features.enterprise_support.api import add_enterprise_customer_to_session
from enterprise.api.v1.serializers import EnterpriseCustomerSerializer from enterprise.api.v1.serializers import EnterpriseCustomerSerializer
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy() FEATURES_WITH_DISABLE_HONOR_CERTIFICATE = settings.FEATURES.copy()
FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True FEATURES_WITH_DISABLE_HONOR_CERTIFICATE['DISABLE_HONOR_CERTIFICATES'] = True
@@ -1283,8 +1283,8 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertContains(resp, "earned a certificate for this course.") self.assertContains(resp, "earned a certificate for this course.")
@ddt.data( @ddt.data(
(True, 54), (True, 56),
(False, 54), (False, 56),
) )
@ddt.unpack @ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count): def test_progress_queries_paced_courses(self, self_paced, query_count):
@@ -1299,7 +1299,7 @@ class ProgressPageTests(ProgressPageBaseTests):
ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) ContentTypeGatingConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1))
self.setup_course() self.setup_course()
with self.assertNumQueries( with self.assertNumQueries(
54, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST 56, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
), check_mongo_calls(2): ), check_mongo_calls(2):
self._get_progress_page() self._get_progress_page()

View File

@@ -156,8 +156,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
assert mock_block_structure_create.call_count == 1 assert mock_block_structure_create.call_count == 1
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.split, 1, 42, True), (ModuleStoreEnum.Type.split, 1, 47, True),
(ModuleStoreEnum.Type.split, 1, 42, False), (ModuleStoreEnum.Type.split, 1, 47, False),
) )
@ddt.unpack @ddt.unpack
def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections): def test_query_counts(self, default_store, num_mongo_calls, num_sql_calls, create_multiple_subsections):
@@ -168,7 +168,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
self._apply_recalculate_subsection_grade() self._apply_recalculate_subsection_grade()
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.split, 1, 42), (ModuleStoreEnum.Type.split, 1, 47),
) )
@ddt.unpack @ddt.unpack
def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls): def test_query_counts_dont_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
@@ -256,7 +256,7 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
UserPartition.scheme_extensions = None UserPartition.scheme_extensions = None
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.split, 1, 42), (ModuleStoreEnum.Type.split, 1, 47),
) )
@ddt.unpack @ddt.unpack
def test_persistent_grades_on_course(self, default_store, num_mongo_queries, num_sql_queries): def test_persistent_grades_on_course(self, default_store, num_mongo_queries, num_sql_queries):

View File

@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
from unittest.mock import ANY, MagicMock, Mock, patch from unittest.mock import ANY, MagicMock, Mock, patch
import ddt import ddt
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
import pytest import pytest
import unicodecsv import unicodecsv
from django.conf import settings from django.conf import settings
@@ -85,6 +86,8 @@ _TEAMS_CONFIG = TeamsConfig({
}) })
USE_ON_DISK_GRADE_REPORT = 'lms.djangoapps.instructor_task.tasks_helper.grades.use_on_disk_grade_reporting' USE_ON_DISK_GRADE_REPORT = 'lms.djangoapps.instructor_task.tasks_helper.grades.use_on_disk_grade_reporting'
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase): class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase):
""" Base class for grade report tests. """ """ Base class for grade report tests. """
@@ -411,7 +414,7 @@ class TestInstructorGradeReport(InstructorGradeReportTestCase):
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'): with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(2): with check_mongo_calls(2):
with self.assertNumQueries(46): with self.assertNumQueries(48, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
CourseGradeReport.generate(None, None, course.id, {}, 'graded') CourseGradeReport.generate(None, None, course.id, {}, 'graded')
def test_inactive_enrollments(self): def test_inactive_enrollments(self):
@@ -2215,7 +2218,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 0, 'failed': 0,
'skipped': 2 'skipped': 2
} }
with self.assertNumQueries(61): with self.assertNumQueries(69, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
self.assertCertificatesGenerated(task_input, expected_results) self.assertCertificatesGenerated(task_input, expected_results)
@ddt.data( @ddt.data(

View File

@@ -10,10 +10,14 @@ from django.test.client import RequestFactory
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from lms.djangoapps.teams.serializers import BulkTeamCountTopicSerializer, MembershipSerializer, TopicSerializer from lms.djangoapps.teams.serializers import BulkTeamCountTopicSerializer, MembershipSerializer, TopicSerializer
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.core.lib.teams_config import TeamsConfig from openedx.core.lib.teams_config import TeamsConfig
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
class SerializerTestCase(SharedModuleStoreTestCase): class SerializerTestCase(SharedModuleStoreTestCase):
""" """
@@ -75,7 +79,9 @@ class TopicSerializerTestCase(SerializerTestCase):
Verifies that the `TopicSerializer` correctly displays a topic with a Verifies that the `TopicSerializer` correctly displays a topic with a
team count of 0, and that it takes a known number of SQL queries. team count of 0, and that it takes a known number of SQL queries.
""" """
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer( serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data, self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id}, context={'course_id': self.course.id},
@@ -91,7 +97,9 @@ class TopicSerializerTestCase(SerializerTestCase):
CourseTeamFactory.create( CourseTeamFactory.create(
course_id=self.course.id, topic_id=self.course.teamsets[0].teamset_id course_id=self.course.id, topic_id=self.course.teamsets[0].teamset_id
) )
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer( serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data, self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id}, context={'course_id': self.course.id},
@@ -110,7 +118,9 @@ class TopicSerializerTestCase(SerializerTestCase):
) )
CourseTeamFactory.create(course_id=self.course.id, topic_id=duplicate_topic['id']) CourseTeamFactory.create(course_id=self.course.id, topic_id=duplicate_topic['id'])
CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id']) CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id'])
with self.assertNumQueries(3): # 2 split modulestore MySQL queries, 1 for Teams with self.assertNumQueries(
3, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # 2 split modulestore MySQL queries, 1 for Teams
serializer = TopicSerializer( serializer = TopicSerializer(
self.course.teamsets[0].cleaned_data, self.course.teamsets[0].cleaned_data,
context={'course_id': self.course.id}, context={'course_id': self.course.id},
@@ -163,7 +173,7 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
""" """
Verify that the serializer produced the expected topics. Verify that the serializer produced the expected topics.
""" """
with self.assertNumQueries(num_queries): with self.assertNumQueries(num_queries, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
page = Paginator( page = Paginator(
self.course.teams_configuration.cleaned_data['teamsets'], self.course.teams_configuration.cleaned_data['teamsets'],
self.PAGE_SIZE, self.PAGE_SIZE,
@@ -203,7 +213,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
query. query.
""" """
topics = self.setup_topics(teams_per_topic=0) topics = self.setup_topics(teams_per_topic=0)
self.assert_serializer_output(topics, num_teams_per_topic=0, num_queries=2) self.assert_serializer_output(topics, num_teams_per_topic=0, num_queries=4)
def test_topics_with_team_counts(self): def test_topics_with_team_counts(self):
""" """
@@ -212,7 +222,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
""" """
teams_per_topic = 10 teams_per_topic = 10
topics = self.setup_topics(teams_per_topic=teams_per_topic) topics = self.setup_topics(teams_per_topic=teams_per_topic)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=2) self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def test_subset_of_topics(self): def test_subset_of_topics(self):
""" """
@@ -221,7 +231,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
""" """
teams_per_topic = 10 teams_per_topic = 10
topics = self.setup_topics(num_topics=self.NUM_TOPICS, teams_per_topic=teams_per_topic) topics = self.setup_topics(num_topics=self.NUM_TOPICS, teams_per_topic=teams_per_topic)
self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=2) self.assert_serializer_output(topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def test_scoped_within_course(self): def test_scoped_within_course(self):
"""Verify that team counts are scoped within a course.""" """Verify that team counts are scoped within a course."""
@@ -235,7 +245,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
}), }),
) )
CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id']) CourseTeamFactory.create(course_id=second_course.id, topic_id=duplicate_topic['id'])
self.assert_serializer_output(first_course_topics, num_teams_per_topic=teams_per_topic, num_queries=2) self.assert_serializer_output(first_course_topics, num_teams_per_topic=teams_per_topic, num_queries=4)
def _merge_dicts(self, first, second): def _merge_dicts(self, first, second):
"""Convenience method to merge two dicts in a single expression""" """Convenience method to merge two dicts in a single expression"""
@@ -251,7 +261,9 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
request = RequestFactory().get('/api/team/v0/topics') request = RequestFactory().get('/api/team/v0/topics')
request.user = self.user request.user = self.user
with self.assertNumQueries(num_queries + 2): # num_queries on teams tables, plus 2 split modulestore queries with self.assertNumQueries(
num_queries + 2, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
): # num_queries on teams tables, plus 2 split modulestore queries
serializer = self.serializer( serializer = self.serializer(
topics, topics,
context={ context={
@@ -269,4 +281,4 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
with no topics. with no topics.
""" """
self.course.teams_configuration = TeamsConfig({'topics': []}) self.course.teams_configuration = TeamsConfig({'topics': []})
self.assert_serializer_output([], num_teams_per_topic=0, num_queries=1) self.assert_serializer_output([], num_teams_per_topic=0, num_queries=3)

View File

@@ -514,12 +514,12 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
@ddt.data( @ddt.data(
('staff', 11), ('staff', 11),
("content_creatorA", 22), ("content_creatorA", 23),
("library_staffA", 22), ("library_staffA", 23),
("library_userA", 22), ("library_userA", 23),
("instructorA", 22), ("instructorA", 23),
("course_instructorA", 22), ("course_instructorA", 23),
("course_staffA", 22), ("course_staffA", 23),
) )
@ddt.unpack @ddt.unpack
def test_list_taxonomy_query_count(self, user_attr: str, expected_queries: int): def test_list_taxonomy_query_count(self, user_attr: str, expected_queries: int):
@@ -1947,16 +1947,16 @@ class TestObjectTagViewSet(TestObjectTagMixin, APITestCase):
('staff', 'courseA', 8), ('staff', 'courseA', 8),
('staff', 'libraryA', 8), ('staff', 'libraryA', 8),
('staff', 'collection_key', 8), ('staff', 'collection_key', 8),
("content_creatorA", 'courseA', 17, False), ("content_creatorA", 'courseA', 18, False),
("content_creatorA", 'libraryA', 17, False), ("content_creatorA", 'libraryA', 18, False),
("content_creatorA", 'collection_key', 17, False), ("content_creatorA", 'collection_key', 18, False),
("library_staffA", 'libraryA', 17, False), # Library users can only view objecttags, not change them? ("library_staffA", 'libraryA', 18, False), # Library users can only view objecttags, not change them?
("library_staffA", 'collection_key', 17, False), ("library_staffA", 'collection_key', 18, False),
("library_userA", 'libraryA', 17, False), ("library_userA", 'libraryA', 18, False),
("library_userA", 'collection_key', 17, False), ("library_userA", 'collection_key', 18, False),
("instructorA", 'courseA', 17), ("instructorA", 'courseA', 18),
("course_instructorA", 'courseA', 17), ("course_instructorA", 'courseA', 18),
("course_staffA", 'courseA', 17), ("course_staffA", 'courseA', 18),
) )
@ddt.unpack @ddt.unpack
def test_object_tags_query_count( def test_object_tags_query_count(

View File

@@ -19,7 +19,8 @@ from django.test.utils import override_settings
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, mixed_store_config
from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, skip_unless_lms
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from common.djangoapps.student.tests.factories import UserFactory from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.student.roles import ( from common.djangoapps.student.roles import (
GlobalStaff, CourseRole, OrgRole, GlobalStaff, CourseRole, OrgRole,
@@ -35,6 +36,7 @@ from ..models import (
from .. import api as embargo_api from .. import api as embargo_api
from ..exceptions import InvalidAccessPoint from ..exceptions import InvalidAccessPoint
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}) MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
@@ -175,10 +177,10 @@ class EmbargoCheckAccessApiTests(ModuleStoreTestCase):
# (restricted course, but pass all the checks) # (restricted course, but pass all the checks)
# This is the worst case, so it will hit all of the # This is the worst case, so it will hit all of the
# caching code. # caching code.
with self.assertNumQueries(3): with self.assertNumQueries(5, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
embargo_api.check_course_access(self.course.id, user=self.user, ip_addresses=['0.0.0.0']) embargo_api.check_course_access(self.course.id, user=self.user, ip_addresses=['0.0.0.0'])
with self.assertNumQueries(0): with self.assertNumQueries(0, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
embargo_api.check_course_access(self.course.id, user=self.user, ip_addresses=['0.0.0.0']) embargo_api.check_course_access(self.course.id, user=self.user, ip_addresses=['0.0.0.0'])
def test_caching_no_restricted_courses(self): def test_caching_no_restricted_courses(self):

View File

@@ -12,6 +12,7 @@ from zoneinfo import ZoneInfo
from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.course_modes.tests.factories import CourseModeFactory from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.roles import AuthzCompatCourseAccessRole
from openedx.core.djangoapps.enrollments import data from openedx.core.djangoapps.enrollments import data
from openedx.core.djangoapps.enrollments.errors import ( from openedx.core.djangoapps.enrollments.errors import (
CourseEnrollmentClosedError, CourseEnrollmentClosedError,
@@ -387,8 +388,15 @@ class EnrollmentDataTest(ModuleStoreTestCase):
expected_role = CourseAccessRoleFactory.create( expected_role = CourseAccessRoleFactory.create(
course_id=self.course.id, user=self.user, role="SuperCoolTestRole", course_id=self.course.id, user=self.user, role="SuperCoolTestRole",
) )
expected_role_compat = AuthzCompatCourseAccessRole(
user_id=expected_role.user.id,
username=expected_role.user.username,
org=expected_role.org,
course_id=expected_role.course_id,
role=expected_role.role,
)
roles = data.get_user_roles(self.user.username) roles = data.get_user_roles(self.user.username)
assert roles == {expected_role} assert roles == {expected_role_compat}
def test_get_roles_no_roles(self): def test_get_roles_no_roles(self):
"""Get roles for a user who has no roles""" """Get roles for a user who has no roles"""

View File

@@ -34,8 +34,11 @@ from openedx.core.djangoapps.schedules.resolvers import (
) )
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory, SiteFactory
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import CacheIsolationMixin, skip_unless_lms from openedx.core.djangolib.testing.utils import CacheIsolationMixin, skip_unless_lms
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
class SchedulesResolverTestMixin(CacheIsolationMixin): class SchedulesResolverTestMixin(CacheIsolationMixin):
""" """
@@ -276,7 +279,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
def test_schedule_context(self): def test_schedule_context(self):
resolver = self.create_resolver() resolver = self.create_resolver()
# using this to make sure the select_related stays intact # using this to make sure the select_related stays intact
with self.assertNumQueries(26): with self.assertNumQueries(22, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
sc = resolver.get_schedules() sc = resolver.get_schedules()
schedules = list(sc) schedules = list(sc)
apple_logo_url = 'http://email-media.s3.amazonaws.com/edX/2021/store_apple_229x78.jpg' apple_logo_url = 'http://email-media.s3.amazonaws.com/edX/2021/store_apple_229x78.jpg'

View File

@@ -25,6 +25,13 @@ from edx_django_utils.cache import RequestCache
from openedx.core.lib import ensure_cms, ensure_lms from openedx.core.lib import ensure_cms, ensure_lms
# Used to ignore queries against authz tables when using assertNumQueries in FilteredQueryCountMixin
AUTHZ_TABLES = [
"casbin_rule",
"openedx_authz_policycachecontrol",
"django_migrations",
]
class CacheIsolationMixin: class CacheIsolationMixin:
""" """
@@ -182,9 +189,9 @@ class _AssertNumQueriesContext(CaptureQueriesContext):
if self.table_ignorelist: if self.table_ignorelist:
for table in self.table_ignorelist: for table in self.table_ignorelist:
# SQL contains the following format for columns: # SQL contains the following format for columns:
# "table_name"."column_name". The regex ensures there is no # "table_name"."column_name" or table_name.column_name.
# "." before the name to avoid matching columns. # The regex ensures there is no "." before the name to avoid matching columns.
if re.search(fr'[^.]"{table}"', query['sql']): if re.search(fr'[^."]"?{table}"?', query['sql']):
return False return False
return True return True

View File

@@ -16,13 +16,16 @@ from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.config_model_utils.models import Provenance from openedx.core.djangoapps.config_model_utils.models import Provenance
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, CacheIsolationTestCase, FilteredQueryCountMixin
from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@ddt.ddt @ddt.ddt
class TestContentTypeGatingConfig(CacheIsolationTestCase): # pylint: disable=missing-class-docstring class TestContentTypeGatingConfig(FilteredQueryCountMixin, CacheIsolationTestCase): # pylint: disable=missing-class-docstring
ENABLED_CACHES = ['default'] ENABLED_CACHES = ['default']
@@ -71,9 +74,9 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): # pylint: disable=mi
user = self.user user = self.user
course_key = self.course_overview.id course_key = self.course_overview.id
query_count = 7 query_count = 9
with self.assertNumQueries(query_count): with self.assertNumQueries(query_count, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
enabled = ContentTypeGatingConfig.enabled_for_enrollment( enabled = ContentTypeGatingConfig.enabled_for_enrollment(
user=user, user=user,
course_key=course_key, course_key=course_key,

View File

@@ -18,12 +18,15 @@ from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, U
from openedx.core.djangoapps.config_model_utils.models import Provenance from openedx.core.djangoapps.config_model_utils.models import Provenance
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, CacheIsolationTestCase, FilteredQueryCountMixin
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
@ddt.ddt @ddt.ddt
class TestCourseDurationLimitConfig(CacheIsolationTestCase): class TestCourseDurationLimitConfig(FilteredQueryCountMixin, CacheIsolationTestCase):
""" """
Tests of CourseDurationLimitConfig Tests of CourseDurationLimitConfig
""" """
@@ -74,9 +77,9 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
user = self.user user = self.user
course_key = self.course_overview.id # lint-amnesty, pylint: disable=unused-variable course_key = self.course_overview.id # lint-amnesty, pylint: disable=unused-variable
query_count = 7 query_count = 9
with self.assertNumQueries(query_count): with self.assertNumQueries(query_count, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
enabled = CourseDurationLimitConfig.enabled_for_enrollment(user, self.course_overview) enabled = CourseDurationLimitConfig.enabled_for_enrollment(user, self.course_overview)
assert (not enrolled_before_enabled) == enabled assert (not enrolled_before_enabled) == enabled

View File

@@ -8,11 +8,12 @@ from django.urls import reverse
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES from openedx.core.djangoapps.waffle_utils.testutils import WAFFLE_TABLES
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase from openedx.features.course_experience.tests import BaseCourseUpdatesTestCase
from xmodule.modulestore.tests.factories import check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import check_mongo_calls # lint-amnesty, pylint: disable=wrong-import-order
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
def course_updates_url(course): def course_updates_url(course):
@@ -49,7 +50,7 @@ class TestCourseUpdatesPage(BaseCourseUpdatesTestCase):
# Fetch the view and verify that the query counts haven't changed # Fetch the view and verify that the query counts haven't changed
# TODO: decrease query count as part of REVO-28 # TODO: decrease query count as part of REVO-28
with self.assertNumQueries(52, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST): with self.assertNumQueries(54, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
with check_mongo_calls(3): with check_mongo_calls(3):
url = course_updates_url(self.course) url = course_updates_url(self.course)
self.client.get(url) self.client.get(url)

View File

@@ -820,7 +820,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels # enterprise-integrated-channels
# openedx-authz # openedx-authz
# openedx-forum # openedx-forum
openedx-authz==0.22.0 openedx-authz==0.23.0
# via -r requirements/edx/kernel.in # via -r requirements/edx/kernel.in
openedx-calc==4.0.3 openedx-calc==4.0.3
# via # via

View File

@@ -1373,7 +1373,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels # enterprise-integrated-channels
# openedx-authz # openedx-authz
# openedx-forum # openedx-forum
openedx-authz==0.22.0 openedx-authz==0.23.0
# via # via
# -r requirements/edx/doc.txt # -r requirements/edx/doc.txt
# -r requirements/edx/testing.txt # -r requirements/edx/testing.txt

View File

@@ -1000,7 +1000,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels # enterprise-integrated-channels
# openedx-authz # openedx-authz
# openedx-forum # openedx-forum
openedx-authz==0.22.0 openedx-authz==0.23.0
# via -r requirements/edx/base.txt # via -r requirements/edx/base.txt
openedx-calc==4.0.3 openedx-calc==4.0.3
# via # via

View File

@@ -1050,7 +1050,7 @@ openedx-atlas==0.7.0
# enterprise-integrated-channels # enterprise-integrated-channels
# openedx-authz # openedx-authz
# openedx-forum # openedx-forum
openedx-authz==0.22.0 openedx-authz==0.23.0
# via -r requirements/edx/base.txt # via -r requirements/edx/base.txt
openedx-calc==4.0.3 openedx-calc==4.0.3
# via # via