feat: AuthZ for course authoring compatibility layer (#38013)
This commit is contained in:
@@ -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.tests.factories import CourseOverviewFactory
|
||||
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.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
|
||||
@@ -42,6 +43,8 @@ from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, p
|
||||
TOTAL_COURSES_COUNT = 10
|
||||
USER_COURSES_COUNT = 1
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseListing(ModuleStoreTestCase):
|
||||
@@ -303,10 +306,10 @@ class TestCourseListing(ModuleStoreTestCase):
|
||||
courses_list, __ = _accessible_courses_list_from_groups(self.request)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def test_course_listing_errored_deleted_courses(self):
|
||||
|
||||
@@ -73,24 +73,23 @@ def _user_can_create_library_for_org(user, org=None):
|
||||
elif user.is_staff:
|
||||
return True
|
||||
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'
|
||||
has_org_staff_role = OrgStaffRole().get_orgs_for_user(user).filter(**org_filter_params).exists()
|
||||
has_course_staff_role = (
|
||||
UserBasedRole(user=user, role=CourseStaffRole.ROLE)
|
||||
.courses_with_role()
|
||||
.filter(**org_filter_params)
|
||||
.exists()
|
||||
)
|
||||
has_course_admin_role = (
|
||||
UserBasedRole(user=user, role=CourseInstructorRole.ROLE)
|
||||
.courses_with_role()
|
||||
.filter(**org_filter_params)
|
||||
.exists()
|
||||
)
|
||||
return is_course_creator or has_org_staff_role or has_course_staff_role or has_course_admin_role
|
||||
if is_course_creator:
|
||||
return True
|
||||
|
||||
has_org_staff_role = OrgStaffRole().has_org_for_user(user, org)
|
||||
if has_org_staff_role:
|
||||
return True
|
||||
|
||||
has_course_staff_role = UserBasedRole(user=user, role=CourseStaffRole.ROLE).has_courses_with_role(org)
|
||||
if has_course_staff_role:
|
||||
return True
|
||||
|
||||
has_course_admin_role = UserBasedRole(user=user, role=CourseInstructorRole.ROLE).has_courses_with_role(org)
|
||||
if has_course_admin_role:
|
||||
return True
|
||||
|
||||
return False
|
||||
else:
|
||||
# EDUCATOR-1924: DISABLE_LIBRARY_CREATION overrides DISABLE_COURSE_CREATION, if present.
|
||||
disable_library_creation = settings.FEATURES.get('DISABLE_LIBRARY_CREATION', None)
|
||||
|
||||
@@ -14,7 +14,7 @@ from openedx.core.djangoapps.django_comment_common.models import (
|
||||
)
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from common.djangoapps.student.roles import (
|
||||
CourseAccessRole,
|
||||
AuthzCompatCourseAccessRole,
|
||||
CourseBetaTesterRole,
|
||||
CourseInstructorRole,
|
||||
CourseStaffRole,
|
||||
@@ -66,7 +66,7 @@ def get_role_cache(user: User) -> RoleCache:
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
|
||||
@@ -4,16 +4,23 @@ adding users, removing users, and listing members
|
||||
"""
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
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.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 openedx.core.lib.cache_utils import get_cache
|
||||
from openedx.core.toggles import enable_authz_course_authoring
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,6 +34,46 @@ ACCESS_ROLES_INHERITANCE = {}
|
||||
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):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
"""
|
||||
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))
|
||||
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'):
|
||||
user_id = role.user.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]
|
||||
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]
|
||||
for user in users_without_roles:
|
||||
@@ -117,7 +217,7 @@ class BulkRoleCache: # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
|
||||
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;
|
||||
don't use `_roles_by_course_id` or `_roles` directly.
|
||||
_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)
|
||||
except KeyError:
|
||||
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()
|
||||
for role in roles:
|
||||
course_id = get_role_cache_key_for_course(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(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()
|
||||
for roles_for_course in self._roles_by_course_id.values():
|
||||
self._roles.update(roles_for_course)
|
||||
|
||||
@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.
|
||||
"""
|
||||
@@ -269,13 +386,33 @@ class RoleBase(AccessRole):
|
||||
|
||||
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.
|
||||
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
|
||||
# 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:
|
||||
if user.is_authenticated and user.is_active:
|
||||
CourseAccessRole.objects.get_or_create(
|
||||
@@ -284,9 +421,38 @@ class RoleBase(AccessRole):
|
||||
if hasattr(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.
|
||||
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(
|
||||
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'):
|
||||
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
|
||||
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
|
||||
if self.course_key is None:
|
||||
@@ -310,12 +500,63 @@ class RoleBase(AccessRole):
|
||||
)
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -329,9 +570,25 @@ class CourseRole(RoleBase):
|
||||
super().__init__(role, course_key.org, course_key)
|
||||
|
||||
@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()
|
||||
|
||||
@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):
|
||||
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
|
||||
"""
|
||||
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:
|
||||
entry = CourseAccessRole(user=self.user, role=self.role, course_id=course_key, org=course_key.org)
|
||||
entry.save()
|
||||
if enable_authz_course_authoring(course_key):
|
||||
# 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'):
|
||||
del self.user._roles
|
||||
else:
|
||||
@@ -531,18 +797,102 @@ class UserBasedRole:
|
||||
"""
|
||||
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.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'):
|
||||
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
|
||||
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 a set of AuthzCompatCourseAccessRole for all of the courses with this user x (or derived from x) role.
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -6,12 +6,17 @@ Tests of student.roles
|
||||
import ddt
|
||||
from django.contrib.auth.models import Permission
|
||||
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.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.models import CourseAccessRoleHistory, User
|
||||
from common.djangoapps.student.roles import (
|
||||
AuthzCompatCourseAccessRole,
|
||||
CourseAccessRole,
|
||||
CourseBetaTesterRole,
|
||||
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.tests.factories import AnonymousUserFactory, InstructorFactory, StaffFactory, UserFactory
|
||||
from openedx.core.toggles import AUTHZ_COURSE_AUTHORING_FLAG
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class RolesTestCase(TestCase):
|
||||
"""
|
||||
Tests of student.roles
|
||||
@@ -41,8 +48,10 @@ class RolesTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._seed_database_with_policies()
|
||||
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 = CourseOverviewFactory.create(id=self.course_key)
|
||||
self.anonymous_user = AnonymousUserFactory()
|
||||
self.student = UserFactory()
|
||||
self.global_staff = UserFactory(is_staff=True)
|
||||
@@ -50,37 +59,67 @@ class RolesTestCase(TestCase):
|
||||
self.course_instructor = InstructorFactory(course_key=self.course_key)
|
||||
self.orgs = ["Marvel", "DC"]
|
||||
|
||||
def test_global_staff(self):
|
||||
assert not GlobalStaff().has_user(self.student)
|
||||
assert not GlobalStaff().has_user(self.course_staff)
|
||||
assert not GlobalStaff().has_user(self.course_instructor)
|
||||
assert GlobalStaff().has_user(self.global_staff)
|
||||
@classmethod
|
||||
def _seed_database_with_policies(cls):
|
||||
"""Seed the database with policies from the policy file for openedx_authz tests.
|
||||
|
||||
def test_has_staff_roles(self):
|
||||
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)
|
||||
This simulates the one-time database seeding that would happen
|
||||
during application deployment, separate from the runtime policy loading.
|
||||
"""
|
||||
import pkg_resources
|
||||
from openedx_authz.engine.utils import migrate_policy_between_enforcers
|
||||
import casbin
|
||||
|
||||
def test_get_course_roles(self):
|
||||
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)) == [
|
||||
CourseAccessRole(
|
||||
user=self.course_staff,
|
||||
course_id=self.course_key,
|
||||
org=self.course_key.org,
|
||||
role=CourseStaffRole.ROLE,
|
||||
)
|
||||
]
|
||||
assert list(get_course_roles(self.course_instructor)) == [
|
||||
CourseAccessRole(
|
||||
user=self.course_instructor,
|
||||
course_id=self.course_key,
|
||||
org=self.course_key.org,
|
||||
role=CourseInstructorRole.ROLE,
|
||||
)
|
||||
]
|
||||
global_enforcer = AuthzEnforcer.get_enforcer()
|
||||
global_enforcer.load_policy()
|
||||
model_path = pkg_resources.resource_filename("openedx_authz.engine", "config/model.conf")
|
||||
policy_path = pkg_resources.resource_filename("openedx_authz.engine", "config/authz.policy")
|
||||
|
||||
migrate_policy_between_enforcers(
|
||||
source_enforcer=casbin.Enforcer(model_path, policy_path),
|
||||
target_enforcer=global_enforcer,
|
||||
)
|
||||
global_enforcer.clear_policy() # Clear to simulate fresh start for each test
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_global_staff(self, authz_enabled):
|
||||
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
|
||||
assert not GlobalStaff().has_user(self.student)
|
||||
assert not GlobalStaff().has_user(self.course_staff)
|
||||
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):
|
||||
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 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
|
||||
"""
|
||||
assert not CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f'Student has premature access to {self.course_key}'
|
||||
CourseStaffRole(self.course_key).add_users(self.student)
|
||||
assert CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f"Student doesn't have access to {str(self.course_key)}"
|
||||
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
|
||||
assert not CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f'Student has premature access to {self.course_key}'
|
||||
CourseStaffRole(self.course_key).add_users(self.student)
|
||||
assert CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f"Student doesn't have access to {str(self.course_key)}"
|
||||
|
||||
# remove access and confirm
|
||||
CourseStaffRole(self.course_key).remove_users(self.student)
|
||||
assert not CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f'Student still has access to {self.course_key}'
|
||||
# remove access and confirm
|
||||
CourseStaffRole(self.course_key).remove_users(self.student)
|
||||
assert not CourseStaffRole(self.course_key).has_user(self.student), \
|
||||
f'Student still has access to {self.course_key}'
|
||||
|
||||
def test_org_role(self):
|
||||
"""
|
||||
@@ -158,26 +199,30 @@ class RolesTestCase(TestCase):
|
||||
assert not CourseInstructorRole(self.course_key).has_user(self.student), \
|
||||
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
|
||||
"""
|
||||
role = CourseStaffRole(self.course_key)
|
||||
role.add_users(self.student)
|
||||
assert len(role.users_with_role()) > 0
|
||||
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
|
||||
role = CourseStaffRole(self.course_key)
|
||||
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
|
||||
to remove_users does not result in the user remaining in the group.
|
||||
"""
|
||||
role = CourseStaffRole(self.course_key)
|
||||
role.add_users(self.student)
|
||||
assert role.has_user(self.student)
|
||||
# Call add_users a second time, then remove just once.
|
||||
role.add_users(self.student)
|
||||
role.remove_users(self.student)
|
||||
assert not role.has_user(self.student)
|
||||
with override_waffle_flag(AUTHZ_COURSE_AUTHORING_FLAG, active=authz_enabled):
|
||||
role = CourseStaffRole(self.course_key)
|
||||
role.add_users(self.student)
|
||||
assert role.has_user(self.student)
|
||||
# Call add_users a second time, then remove just once.
|
||||
role.add_users(self.student)
|
||||
role.remove_users(self.student)
|
||||
assert not role.has_user(self.student)
|
||||
|
||||
def test_get_orgs_for_user(self):
|
||||
"""
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
|
||||
import pytest
|
||||
from ccx_keys.locator import CCXLocator
|
||||
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.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
@mock.patch.dict(
|
||||
@@ -234,7 +235,7 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
# TODO: decrease query count as part of REVO-28
|
||||
QUERY_COUNT = 34
|
||||
QUERY_COUNT = 36
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (QUERY_COUNT, 2),
|
||||
|
||||
@@ -10,6 +10,7 @@ from django.test.client import RequestFactory
|
||||
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
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.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
|
||||
@@ -17,6 +18,8 @@ from xmodule.modulestore.tests.sample_courses import BlockInfo # lint-amnesty,
|
||||
|
||||
from ..api import get_blocks
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
|
||||
|
||||
|
||||
class TestGetBlocks(SharedModuleStoreTestCase):
|
||||
"""
|
||||
@@ -196,7 +199,7 @@ class TestGetBlocksQueryCountsBase(SharedModuleStoreTestCase):
|
||||
get_blocks on the given course.
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
@@ -212,11 +215,11 @@ class TestGetBlocksQueryCounts(TestGetBlocksQueryCountsBase):
|
||||
self._get_blocks(
|
||||
course,
|
||||
expected_mongo_queries=0,
|
||||
expected_sql_queries=14,
|
||||
expected_sql_queries=16,
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 2, 24),
|
||||
(ModuleStoreEnum.Type.split, 2, 26),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_query_counts_uncached(self, store_type, expected_mongo_queries, num_sql_queries):
|
||||
|
||||
@@ -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
|
||||
from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers
|
||||
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 ..milestones import MilestonesAndSpecialExamsTransformer
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@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.
|
||||
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)
|
||||
|
||||
# clear the request cache to simulate a new request
|
||||
@@ -184,7 +188,7 @@ class MilestonesTransformerTestCase(CourseStructureTestCase, MilestonesTestCaseM
|
||||
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)
|
||||
|
||||
def test_staff_access(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ import datetime
|
||||
import itertools
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
|
||||
import pytest
|
||||
import ddt
|
||||
import pytz
|
||||
@@ -70,7 +71,7 @@ from openedx.features.enterprise_support.tests.factories import (
|
||||
)
|
||||
from crum import set_current_request
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -878,16 +879,16 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
|
||||
if user_attr_name == 'user_staff' and action == 'see_exists':
|
||||
# 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':
|
||||
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:
|
||||
# checks staff role and enrollment data
|
||||
num_queries = 2
|
||||
num_queries = 4
|
||||
elif user_attr_name == 'user_anonymous' and action == 'see_exists':
|
||||
if course_attr_name == 'course_started':
|
||||
num_queries = 1
|
||||
@@ -896,7 +897,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
else:
|
||||
# if the course has started, check the duration configuration
|
||||
if action == 'see_exists' and course_attr_name == 'course_started':
|
||||
num_queries = 3
|
||||
num_queries = 5
|
||||
else:
|
||||
num_queries = 0
|
||||
|
||||
@@ -950,17 +951,17 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
|
||||
if user_attr_name == 'user_staff':
|
||||
if course_attr_name == 'course_started':
|
||||
# read: CourseAccessRole + django_comment_client.Role
|
||||
num_queries = 2
|
||||
num_queries = 4
|
||||
else:
|
||||
# read: CourseAccessRole + EnterpriseCourseEnrollment
|
||||
num_queries = 2
|
||||
num_queries = 4
|
||||
elif user_attr_name == 'user_normal':
|
||||
if course_attr_name == 'course_started':
|
||||
# read: CourseAccessRole + django_comment_client.Role + FBEEnrollmentExclusion + CourseMode
|
||||
num_queries = 4
|
||||
num_queries = 6
|
||||
else:
|
||||
# read: CourseAccessRole + CourseEnrollmentAllowed + EnterpriseCourseEnrollment
|
||||
num_queries = 3
|
||||
num_queries = 5
|
||||
elif user_attr_name == 'user_anonymous':
|
||||
if course_attr_name == 'course_started':
|
||||
# read: CourseMode
|
||||
|
||||
@@ -4,6 +4,7 @@ Test for lms courseware app, module data (runtime data storage for XBlocks)
|
||||
import json
|
||||
from functools import partial
|
||||
from unittest.mock import Mock, patch
|
||||
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES, FilteredQueryCountMixin
|
||||
import pytest
|
||||
|
||||
from django.db import connections, DatabaseError
|
||||
@@ -13,6 +14,7 @@ from xblock.exceptions import KeyValueMultiSaveError
|
||||
from xblock.fields import BlockScope, Scope, ScopeIds
|
||||
|
||||
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.models import (
|
||||
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 UserStateSummaryFactory
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
def mock_field(scope, name):
|
||||
field = Mock()
|
||||
@@ -239,7 +243,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
|
||||
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
|
||||
databases = set(connections)
|
||||
|
||||
@@ -276,7 +280,9 @@ class TestMissingStudentModule(TestCase): # lint-amnesty, pylint: disable=missi
|
||||
# on the StudentModule).
|
||||
# Django 1.8 also has a number of other BEGIN and SAVESTATE queries.
|
||||
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')
|
||||
|
||||
assert 1 == sum(len(cache) for cache in self.field_data_cache.cache.values())
|
||||
|
||||
@@ -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.models import CreditCourse, CreditProvider
|
||||
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.lib.url_utils import quote_slashes
|
||||
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 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['DISABLE_HONOR_CERTIFICATES'] = True
|
||||
@@ -1283,8 +1283,8 @@ class ProgressPageTests(ProgressPageBaseTests):
|
||||
self.assertContains(resp, "earned a certificate for this course.")
|
||||
|
||||
@ddt.data(
|
||||
(True, 54),
|
||||
(False, 54),
|
||||
(True, 56),
|
||||
(False, 56),
|
||||
)
|
||||
@ddt.unpack
|
||||
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))
|
||||
self.setup_course()
|
||||
with self.assertNumQueries(
|
||||
54, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
|
||||
56, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST
|
||||
), check_mongo_calls(2):
|
||||
self._get_progress_page()
|
||||
|
||||
|
||||
@@ -156,8 +156,8 @@ class RecalculateSubsectionGradeTest(HasCourseWithProblemsMixin, ModuleStoreTest
|
||||
assert mock_block_structure_create.call_count == 1
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 1, 42, True),
|
||||
(ModuleStoreEnum.Type.split, 1, 42, False),
|
||||
(ModuleStoreEnum.Type.split, 1, 47, True),
|
||||
(ModuleStoreEnum.Type.split, 1, 47, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
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()
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 1, 42),
|
||||
(ModuleStoreEnum.Type.split, 1, 47),
|
||||
)
|
||||
@ddt.unpack
|
||||
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
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 1, 42),
|
||||
(ModuleStoreEnum.Type.split, 1, 47),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_persistent_grades_on_course(self, default_store, num_mongo_queries, num_sql_queries):
|
||||
|
||||
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
|
||||
from unittest.mock import ANY, MagicMock, Mock, patch
|
||||
|
||||
import ddt
|
||||
from openedx.core.djangolib.testing.utils import AUTHZ_TABLES
|
||||
import pytest
|
||||
import unicodecsv
|
||||
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'
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = AUTHZ_TABLES
|
||||
|
||||
|
||||
class InstructorGradeReportTestCase(TestReportMixin, InstructorTaskCourseTestCase):
|
||||
""" 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 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')
|
||||
|
||||
def test_inactive_enrollments(self):
|
||||
@@ -2215,7 +2218,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
'failed': 0,
|
||||
'skipped': 2
|
||||
}
|
||||
with self.assertNumQueries(61):
|
||||
with self.assertNumQueries(69, table_ignorelist=QUERY_COUNT_TABLE_IGNORELIST):
|
||||
self.assertCertificatesGenerated(task_input, expected_results)
|
||||
|
||||
@ddt.data(
|
||||
|
||||
@@ -10,10 +10,14 @@ from django.test.client import RequestFactory
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.teams.serializers import BulkTeamCountTopicSerializer, MembershipSerializer, TopicSerializer
|
||||
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 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
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
class SerializerTestCase(SharedModuleStoreTestCase):
|
||||
"""
|
||||
@@ -75,7 +79,9 @@ class TopicSerializerTestCase(SerializerTestCase):
|
||||
Verifies that the `TopicSerializer` correctly displays a topic with a
|
||||
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(
|
||||
self.course.teamsets[0].cleaned_data,
|
||||
context={'course_id': self.course.id},
|
||||
@@ -91,7 +97,9 @@ class TopicSerializerTestCase(SerializerTestCase):
|
||||
CourseTeamFactory.create(
|
||||
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(
|
||||
self.course.teamsets[0].cleaned_data,
|
||||
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=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(
|
||||
self.course.teamsets[0].cleaned_data,
|
||||
context={'course_id': self.course.id},
|
||||
@@ -163,7 +173,7 @@ class BaseTopicSerializerTestCase(SerializerTestCase):
|
||||
"""
|
||||
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(
|
||||
self.course.teams_configuration.cleaned_data['teamsets'],
|
||||
self.PAGE_SIZE,
|
||||
@@ -203,7 +213,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
|
||||
query.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -212,7 +222,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
|
||||
"""
|
||||
teams_per_topic = 10
|
||||
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):
|
||||
"""
|
||||
@@ -221,7 +231,7 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
|
||||
"""
|
||||
teams_per_topic = 10
|
||||
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):
|
||||
"""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'])
|
||||
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):
|
||||
"""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.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(
|
||||
topics,
|
||||
context={
|
||||
@@ -269,4 +281,4 @@ class BulkTeamCountTopicSerializerTestCase(BaseTopicSerializerTestCase):
|
||||
with no 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)
|
||||
|
||||
@@ -514,12 +514,12 @@ class TestTaxonomyListCreateViewSet(TestTaxonomyObjectsMixin, APITestCase):
|
||||
|
||||
@ddt.data(
|
||||
('staff', 11),
|
||||
("content_creatorA", 22),
|
||||
("library_staffA", 22),
|
||||
("library_userA", 22),
|
||||
("instructorA", 22),
|
||||
("course_instructorA", 22),
|
||||
("course_staffA", 22),
|
||||
("content_creatorA", 23),
|
||||
("library_staffA", 23),
|
||||
("library_userA", 23),
|
||||
("instructorA", 23),
|
||||
("course_instructorA", 23),
|
||||
("course_staffA", 23),
|
||||
)
|
||||
@ddt.unpack
|
||||
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', 'libraryA', 8),
|
||||
('staff', 'collection_key', 8),
|
||||
("content_creatorA", 'courseA', 17, False),
|
||||
("content_creatorA", 'libraryA', 17, False),
|
||||
("content_creatorA", 'collection_key', 17, False),
|
||||
("library_staffA", 'libraryA', 17, False), # Library users can only view objecttags, not change them?
|
||||
("library_staffA", 'collection_key', 17, False),
|
||||
("library_userA", 'libraryA', 17, False),
|
||||
("library_userA", 'collection_key', 17, False),
|
||||
("instructorA", 'courseA', 17),
|
||||
("course_instructorA", 'courseA', 17),
|
||||
("course_staffA", 'courseA', 17),
|
||||
("content_creatorA", 'courseA', 18, False),
|
||||
("content_creatorA", 'libraryA', 18, False),
|
||||
("content_creatorA", 'collection_key', 18, False),
|
||||
("library_staffA", 'libraryA', 18, False), # Library users can only view objecttags, not change them?
|
||||
("library_staffA", 'collection_key', 18, False),
|
||||
("library_userA", 'libraryA', 18, False),
|
||||
("library_userA", 'collection_key', 18, False),
|
||||
("instructorA", 'courseA', 18),
|
||||
("course_instructorA", 'courseA', 18),
|
||||
("course_staffA", 'courseA', 18),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_object_tags_query_count(
|
||||
|
||||
@@ -19,7 +19,8 @@ from django.test.utils import override_settings
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
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.roles import (
|
||||
GlobalStaff, CourseRole, OrgRole,
|
||||
@@ -35,6 +36,7 @@ from ..models import (
|
||||
from .. import api as embargo_api
|
||||
from ..exceptions import InvalidAccessPoint
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {})
|
||||
|
||||
@@ -175,10 +177,10 @@ class EmbargoCheckAccessApiTests(ModuleStoreTestCase):
|
||||
# (restricted course, but pass all the checks)
|
||||
# This is the worst case, so it will hit all of the
|
||||
# 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'])
|
||||
|
||||
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'])
|
||||
|
||||
def test_caching_no_restricted_courses(self):
|
||||
|
||||
@@ -12,6 +12,7 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
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.errors import (
|
||||
CourseEnrollmentClosedError,
|
||||
@@ -387,8 +388,15 @@ class EnrollmentDataTest(ModuleStoreTestCase):
|
||||
expected_role = CourseAccessRoleFactory.create(
|
||||
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)
|
||||
assert roles == {expected_role}
|
||||
assert roles == {expected_role_compat}
|
||||
|
||||
def test_get_roles_no_roles(self):
|
||||
"""Get roles for a user who has no roles"""
|
||||
|
||||
@@ -34,8 +34,11 @@ from openedx.core.djangoapps.schedules.resolvers import (
|
||||
)
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleConfigFactory
|
||||
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
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES
|
||||
|
||||
|
||||
class SchedulesResolverTestMixin(CacheIsolationMixin):
|
||||
"""
|
||||
@@ -276,7 +279,7 @@ class TestCourseNextSectionUpdateResolver(SchedulesResolverTestMixin, ModuleStor
|
||||
def test_schedule_context(self):
|
||||
resolver = self.create_resolver()
|
||||
# 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()
|
||||
schedules = list(sc)
|
||||
apple_logo_url = 'http://email-media.s3.amazonaws.com/edX/2021/store_apple_229x78.jpg'
|
||||
|
||||
@@ -25,6 +25,13 @@ from edx_django_utils.cache import RequestCache
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -182,9 +189,9 @@ class _AssertNumQueriesContext(CaptureQueriesContext):
|
||||
if self.table_ignorelist:
|
||||
for table in self.table_ignorelist:
|
||||
# SQL contains the following format for columns:
|
||||
# "table_name"."column_name". The regex ensures there is no
|
||||
# "." before the name to avoid matching columns.
|
||||
if re.search(fr'[^.]"{table}"', query['sql']):
|
||||
# "table_name"."column_name" or table_name.column_name.
|
||||
# The regex ensures there is no "." before the name to avoid matching columns.
|
||||
if re.search(fr'[^."]"?{table}"?', query['sql']):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -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.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
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 common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestContentTypeGatingConfig(CacheIsolationTestCase): # pylint: disable=missing-class-docstring
|
||||
class TestContentTypeGatingConfig(FilteredQueryCountMixin, CacheIsolationTestCase): # pylint: disable=missing-class-docstring
|
||||
|
||||
ENABLED_CACHES = ['default']
|
||||
|
||||
@@ -71,9 +74,9 @@ class TestContentTypeGatingConfig(CacheIsolationTestCase): # pylint: disable=mi
|
||||
user = self.user
|
||||
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(
|
||||
user=user,
|
||||
course_key=course_key,
|
||||
|
||||
@@ -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.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
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
|
||||
|
||||
QUERY_COUNT_TABLE_IGNORELIST = WAFFLE_TABLES + AUTHZ_TABLES
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCourseDurationLimitConfig(CacheIsolationTestCase):
|
||||
class TestCourseDurationLimitConfig(FilteredQueryCountMixin, CacheIsolationTestCase):
|
||||
"""
|
||||
Tests of CourseDurationLimitConfig
|
||||
"""
|
||||
@@ -74,9 +77,9 @@ class TestCourseDurationLimitConfig(CacheIsolationTestCase):
|
||||
user = self.user
|
||||
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)
|
||||
assert (not enrolled_before_enabled) == enabled
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ from django.urls import reverse
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
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.course_experience.tests import BaseCourseUpdatesTestCase
|
||||
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):
|
||||
@@ -49,7 +50,7 @@ class TestCourseUpdatesPage(BaseCourseUpdatesTestCase):
|
||||
|
||||
# Fetch the view and verify that the query counts haven't changed
|
||||
# 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):
|
||||
url = course_updates_url(self.course)
|
||||
self.client.get(url)
|
||||
|
||||
@@ -820,7 +820,7 @@ openedx-atlas==0.7.0
|
||||
# enterprise-integrated-channels
|
||||
# openedx-authz
|
||||
# openedx-forum
|
||||
openedx-authz==0.22.0
|
||||
openedx-authz==0.23.0
|
||||
# via -r requirements/edx/kernel.in
|
||||
openedx-calc==4.0.3
|
||||
# via
|
||||
|
||||
@@ -1373,7 +1373,7 @@ openedx-atlas==0.7.0
|
||||
# enterprise-integrated-channels
|
||||
# openedx-authz
|
||||
# openedx-forum
|
||||
openedx-authz==0.22.0
|
||||
openedx-authz==0.23.0
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
|
||||
@@ -1000,7 +1000,7 @@ openedx-atlas==0.7.0
|
||||
# enterprise-integrated-channels
|
||||
# openedx-authz
|
||||
# openedx-forum
|
||||
openedx-authz==0.22.0
|
||||
openedx-authz==0.23.0
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-calc==4.0.3
|
||||
# via
|
||||
|
||||
@@ -1050,7 +1050,7 @@ openedx-atlas==0.7.0
|
||||
# enterprise-integrated-channels
|
||||
# openedx-authz
|
||||
# openedx-forum
|
||||
openedx-authz==0.22.0
|
||||
openedx-authz==0.23.0
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-calc==4.0.3
|
||||
# via
|
||||
|
||||
Reference in New Issue
Block a user