Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce number of queries required for content tagging endpoints #34200

Merged
merged 24 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0a3ada3
test: fixes "list taxonomies for org" test URL
pomegranited Feb 7, 2024
8e7823e
fix: filter list of taxonomies by org without fetching the Organization
pomegranited Feb 7, 2024
3057ccf
fix: prefetch tag_set to reduce queries
pomegranited Feb 7, 2024
8a0d598
fix: lint
pomegranited Feb 7, 2024
6e094ef
fix: prefetch taxonomyorg orgs
pomegranited Feb 7, 2024
3cc2f13
Merge branch 'master' into jill/tagging-less-queries
pomegranited Feb 8, 2024
016f2a5
temp: use openedx-learning branch from pr#157
pomegranited Feb 7, 2024
38bef63
fix: iterate over prefetched taxonomyorgs instead of filtering
pomegranited Feb 7, 2024
e581951
fix: Adds a check for the TaxonomyOrg.rel_type when serializing orgs
pomegranited Feb 7, 2024
b46f466
fix: oel_tagging library change reduced query count
pomegranited Feb 8, 2024
5dbae2f
fix: annotate taxonomies with their tags_count
pomegranited Feb 8, 2024
392ccf2
test: adds query count tests for non-taxonomy admins
pomegranited Feb 8, 2024
07031d0
fix: use qs.exists() instead of len(qs) > 0
pomegranited Feb 12, 2024
ab01a79
fix: fetch all content library orgs at once
pomegranited Feb 12, 2024
76b080b
fix: reduced query counts by using the user's RoleCache
pomegranited Feb 12, 2024
85eb4f1
fix: adds content_tagging rules cache for Organization list
pomegranited Feb 12, 2024
8ee319b
fix: adds library orgs cache
pomegranited Feb 12, 2024
05a6fd9
fix: address nits in PR review
pomegranited Feb 15, 2024
011a99b
fix: nit
pomegranited Feb 15, 2024
eac1f26
fix: prevent cross-org ObjectTags from being created (#633)
pomegranited Feb 15, 2024
95861c7
feat: updates openedx-learning dependency
pomegranited Feb 15, 2024
a75a231
Merge branch 'master' into jill/tagging-less-queries
rpenido Feb 16, 2024
3b65f89
fix: missing import
rpenido Feb 16, 2024
0296241
fix: return False if user is anonymous
rpenido Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions openedx/core/djangoapps/content_tagging/rest_api/v1/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from rest_framework.filters import BaseFilterBackend

import openedx_tagging.core.tagging.rules as oel_tagging
from organizations.models import Organization

from ...rules import get_admin_orgs, get_user_orgs
from ...models import TaxonomyOrg
Expand All @@ -25,9 +24,8 @@ def filter_queryset(self, request, queryset, _):
if oel_tagging.is_taxonomy_admin(request.user):
return queryset

orgs = list(Organization.objects.all())
user_admin_orgs = get_admin_orgs(request.user, orgs)
user_orgs = get_user_orgs(request.user, orgs) # Orgs that the user is a content creator or instructor
user_admin_orgs = get_admin_orgs(request.user)
user_orgs = get_user_orgs(request.user) # Orgs that the user is a content creator or instructor

if len(user_orgs) == 0 and len(user_admin_orgs) == 0:
return queryset.none()
Expand Down Expand Up @@ -69,9 +67,8 @@ def filter_queryset(self, request, queryset, _):
if oel_tagging.is_taxonomy_admin(request.user):
return queryset

orgs = list(Organization.objects.all())
user_admin_orgs = get_admin_orgs(request.user, orgs)
user_orgs = get_user_orgs(request.user, orgs)
user_admin_orgs = get_admin_orgs(request.user)
user_orgs = get_user_orgs(request.user)
user_or_admin_orgs = list(set(user_orgs) | set(user_admin_orgs))

return queryset.filter(taxonomy__enabled=True).filter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -499,12 +499,12 @@ def test_create_taxonomy(self, user_attr: str, expected_status: int) -> None:

@ddt.data(
('staff', 11),
("content_creatorA", 19), # FIXME too many queries.
("library_staffA", 19),
("library_userA", 19),
("instructorA", 19),
("course_instructorA", 19),
("course_staffA", 19),
("content_creatorA", 18), # FIXME too many queries.
("library_staffA", 18),
("library_userA", 18),
("instructorA", 18),
("course_instructorA", 18),
("course_staffA", 18),
)
@ddt.unpack
def test_list_taxonomy_query_count(self, user_attr: str, expected_queries: int):
Expand Down Expand Up @@ -1773,10 +1773,10 @@ def test_get_tags(self):
@ddt.data(
('staff', 'courseA', 7),
('staff', 'libraryA', 7),
("content_creatorA", 'courseA', 20, False), # FIXME too many queries.
("content_creatorA", 'libraryA', 20, False),
("library_staffA", 'libraryA', 20, False), # Library users can only view objecttags, not change them.
("library_userA", 'libraryA', 20, False),
("content_creatorA", 'courseA', 15, False), # FIXME too many queries.
("content_creatorA", 'libraryA', 15, False),
("library_staffA", 'libraryA', 15, False), # Library users can only view objecttags, not change them.
("library_userA", 'libraryA', 15, False),
("instructorA", 'courseA', 13),
("course_instructorA", 'courseA', 13),
("course_staffA", 'courseA', 13),
Expand Down
26 changes: 15 additions & 11 deletions openedx/core/djangoapps/content_tagging/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@
from openedx.core.djangoapps.content_libraries.api import get_libraries_for_user

from .models import TaxonomyOrg
from .utils import get_context_key_from_key_string
from .utils import get_context_key_from_key_string, TaggingRulesCache


rules_cache = TaggingRulesCache()
UserType = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser]


def is_org_admin(user: UserType, orgs: list[Organization] | None = None) -> bool:
"""
Return True if the given user is an admin for any of the given orgs.
"""

return len(get_admin_orgs(user, orgs)) > 0


Expand All @@ -47,7 +48,7 @@ def get_admin_orgs(user: UserType, orgs: list[Organization] | None = None) -> li

If no orgs are provided, check all orgs
"""
org_list = Organization.objects.all() if orgs is None else orgs
org_list = rules_cache.get_orgs() if orgs is None else orgs
return [
org for org in org_list if OrgStaffRole(org=org.short_name).has_user(user)
]
Expand Down Expand Up @@ -115,14 +116,15 @@ def _get_library_user_orgs(user: UserType, orgs: list[Organization]) -> list[Org
return list(set(library_orgs).intersection(orgs))


def get_user_orgs(user: UserType, orgs: list[Organization]) -> list[Organization]:
def get_user_orgs(user: UserType, orgs: list[Organization] | None = None) -> list[Organization]:
"""
Return a list of orgs that the given user is a member of (instructor or content creator),
from the given list of orgs.
"""
content_creator_orgs = _get_content_creator_orgs(user, orgs)
course_user_orgs = _get_course_user_orgs(user, orgs)
library_user_orgs = _get_library_user_orgs(user, orgs)
org_list = rules_cache.get_orgs() if orgs is None else orgs
content_creator_orgs = _get_content_creator_orgs(user, org_list)
course_user_orgs = _get_course_user_orgs(user, org_list)
library_user_orgs = _get_library_user_orgs(user, org_list)
user_orgs = list(set(content_creator_orgs) | set(course_user_orgs) | set(library_user_orgs))

return user_orgs
Expand Down Expand Up @@ -235,8 +237,9 @@ def can_change_object_tag_objectid(user: UserType, object_id: str) -> bool:
if has_studio_write_access(user, context_key):
return True

object_org = Organization.objects.filter(short_name=context_key.org).first()
return object_org and is_org_admin(user, [object_org])
assert context_key.org
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and is_org_admin(user, object_org)


@rules.predicate
Expand Down Expand Up @@ -268,8 +271,9 @@ def can_view_object_tag_objectid(user: UserType, object_id: str) -> bool:
if has_studio_read_access(user, context_key):
return True

object_org = Organization.objects.filter(short_name=context_key.org).first()
return object_org and (is_org_admin(user, [object_org]) or is_org_user(user, [object_org]))
assert context_key.org
pomegranited marked this conversation as resolved.
Show resolved Hide resolved
object_org = rules_cache.get_orgs([context_key.org])
return bool(object_org) and (is_org_admin(user, object_org) or is_org_user(user, object_org))


@rules.predicate
Expand Down
36 changes: 36 additions & 0 deletions openedx/core/djangoapps/content_tagging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"""
from __future__ import annotations

from edx_django_utils.cache import RequestCache
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from opaque_keys.edx.locator import LibraryLocatorV2
from organizations.models import Organization

from .types import ContentKey

Expand Down Expand Up @@ -42,3 +44,37 @@ def get_context_key_from_key_string(key_str: str) -> CourseKey | LibraryLocatorV
return context_key

raise ValueError("context must be a CourseKey or a LibraryLocatorV2")


class TaggingRulesCache:
"""
Caches data required for computing rules for the duration of the request.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not have been necessary if we'd have used bridgekeeper instead of rules for defining the permissions. My bad for that original mistake. It's a nice solution for now though :)

"""

def __init__(self):
"""
Initializes the request cache.
"""
self.request_cache = RequestCache('openedx.core.djangoapps.content_tagging.rules')

def get_orgs(self, org_names: list[str] | None = None) -> list[Organization]:
"""
Returns the Organizations with the given name(s), or all Organizations if no names given.

Organization instances are cached for the duration of the request.
"""
cache_key = 'all_orgs'
all_orgs = self.request_cache.data.get(cache_key)
if all_orgs is None:
all_orgs = {
org.short_name: org
for org in Organization.objects.all()
}
self.request_cache.set(cache_key, all_orgs)

if org_names:
return [
all_orgs[org_name] for org_name in org_names if org_name in all_orgs
]

return all_orgs.values()
Loading