Skip to content

Commit 6d0eb9e

Browse files
authored
Search: optimize queryset when searching for subprojects with organizations (#12635)
On .com having organizations introduces some extra queries. Since all subprojects share the same organization as the parent project, we can cache that organization, so it's shared across all subprojects. This also allows caching the .organization property, and skips querying organizations if we are on .org. Results: - One less query for searches on projects with organizations - When searching across subprojects, two queries for each subproject (constant). It used to add 4 queries for each subproject. I had to move tests to .com, so they are more accurate with production, as in .com we do several overrides.
1 parent 7ca089c commit 6d0eb9e

File tree

18 files changed

+127
-32
lines changed

18 files changed

+127
-32
lines changed

readthedocs/api/v3/tests/test_projects.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,10 @@ def test_projects_detail_other_user(self):
212212
response = self.client.get(url, data)
213213
self.assertEqual(response.status_code, 404)
214214

215-
@override_settings(ALLOW_PRIVATE_REPOS=True)
215+
@override_settings(
216+
ALLOW_PRIVATE_REPOS=True,
217+
RTD_ALLOW_ORGANIZATIONS=True,
218+
)
216219
def test_own_projects_detail_privacy_levels_enabled(self):
217220
url = reverse(
218221
"projects-detail",
@@ -227,10 +230,12 @@ def test_own_projects_detail_privacy_levels_enabled(self):
227230
self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}")
228231
response = self.client.get(url, query_params)
229232
self.assertEqual(response.status_code, 200)
230-
self.assertDictEqual(
231-
response.json(),
232-
self._get_response_dict("projects-detail"),
233-
)
233+
234+
expected = self._get_response_dict("projects-detail")
235+
# No users when organzations are enabled
236+
expected.pop("users")
237+
238+
self.assertDictEqual(response.json(), expected)
234239

235240
self.project.privacy_level = "private"
236241
self.project.external_builds_privacy_level = "private"
@@ -245,6 +250,10 @@ def test_own_projects_detail_privacy_levels_enabled(self):
245250
# We don't care about the modified date.
246251
expected.pop("modified")
247252
response.pop("modified")
253+
254+
# No users when organzations are enabled
255+
expected.pop("users")
256+
248257
self.assertDictEqual(response, expected)
249258

250259
def test_projects_superproject_anonymous_user(self):

readthedocs/api/v3/tests/test_subprojects.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@ def test_projects_subprojects_detail_other_user(self):
155155

156156
def test_projects_subprojects_list_post(self):
157157
newproject = self._create_new_project()
158-
self.organization.projects.add(newproject)
159158

160159
self.assertEqual(self.project.subprojects.count(), 1)
161160
url = reverse(

readthedocs/audit/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ def save(self, **kwargs):
223223
if self.project:
224224
self.log_project_id = self.project.id
225225
self.log_project_slug = self.project.slug
226-
organization = self.project.organizations.first()
226+
organization = self.project.organization
227227
if organization:
228228
self.organization = organization
229229
if self.organization:

readthedocs/audit/tests/test_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from django.contrib.auth.models import User
2-
from django.test import TestCase
2+
from django.test import TestCase, override_settings
33
from django_dynamic_fixture import get
44

55
from readthedocs.audit.models import AuditLog
66
from readthedocs.organizations.models import Organization, OrganizationOwner
77
from readthedocs.projects.models import Project
88

99

10+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
1011
class TestAuditModels(TestCase):
1112
def setUp(self):
1213
self.user = get(User)

readthedocs/audit/tests/test_tasks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest import mock
22

33
from django.contrib.auth.models import User
4-
from django.test import TestCase
4+
from django.test import TestCase, override_settings
55
from django.utils import timezone
66
from django_dynamic_fixture import get
77

@@ -11,6 +11,7 @@
1111
from readthedocs.projects.models import Project
1212

1313

14+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
1415
class AuditTasksTest(TestCase):
1516
def setUp(self):
1617
self.user = get(User)

readthedocs/builds/querysets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ def concurrent(self, project):
246246

247247
# If the project belongs to an organization, count all the projects
248248
# from this organization as well
249-
organization = project.organizations.first()
249+
organization = project.organization
250250
if organization:
251251
query |= Q(project__in=organization.projects.all())
252252

readthedocs/builds/tests/test_build_queryset.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.test import override_settings
12
import django_dynamic_fixture as fixture
23
import pytest
34

@@ -79,6 +80,7 @@ def test_concurrent_builds_translations(self):
7980
assert (True, 4, 4) == Build.objects.concurrent(translation)
8081
assert (True, 4, 4) == Build.objects.concurrent(project)
8182

83+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
8284
def test_concurrent_builds_organization(self):
8385
organization = fixture.get(
8486
Organization,
@@ -111,6 +113,7 @@ def test_concurrent_builds_organization(self):
111113
)
112114
assert (True, 6, 4) == Build.objects.concurrent(project)
113115

116+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
114117
def test_concurrent_builds_organization_limited(self):
115118
organization = fixture.get(
116119
Organization,
@@ -138,6 +141,7 @@ def test_concurrent_builds_organization_limited(self):
138141
# from ``project_with_builds`` as well
139142
assert (False, 2, 10) == Build.objects.concurrent(project_without_builds)
140143

144+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
141145
def test_concurrent_builds_organization_and_project_limited(self):
142146
organization = fixture.get(
143147
Organization,

readthedocs/core/permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def owners(cls, obj):
118118

119119
if isinstance(obj, Project):
120120
if settings.RTD_ALLOW_ORGANIZATIONS:
121-
obj = obj.organizations.first()
121+
obj = obj.organization
122122
else:
123123
return obj.users.all()
124124

readthedocs/core/resolver.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,16 @@ def _get_project_domain(self, project, external_version_slug=None, use_canonical
198198
domain = self._get_project_subdomain(canonical_project)
199199
if external_version_slug:
200200
domain = self._get_external_subdomain(canonical_project, external_version_slug)
201-
elif use_canonical_domain and self._use_cname(canonical_project):
201+
# NOTE: call _use_cname only if the project has a canonical custom domain.
202+
# Calling _use_cname is more expensive when we have organizations.
203+
elif (
204+
use_canonical_domain
205+
and canonical_project.canonical_custom_domain
206+
and self._use_cname(canonical_project)
207+
):
202208
domain_object = canonical_project.canonical_custom_domain
203-
if domain_object:
204-
use_https = domain_object.https
205-
domain = domain_object.domain
209+
use_https = domain_object.https
210+
domain = domain_object.domain
206211

207212
return domain, use_https
208213

@@ -365,4 +370,22 @@ def _fix_filename(self, filename):
365370

366371
def _use_cname(self, project):
367372
"""Test if to allow direct serving for project on CNAME."""
373+
# If the project belongs to an organization, use the other
374+
# method, to allow caching the result for that organization.
375+
if project.organization:
376+
return self._organization_allows_custom_domain(project.organization)
377+
368378
return bool(get_feature(project, feature_type=TYPE_CNAME))
379+
380+
@cache
381+
def _organization_allows_custom_domain(self, organization):
382+
"""
383+
Test if the organization allows custom domains.
384+
385+
.. note::
386+
387+
This is done on a separate method, so we can cache the result
388+
for each organization. This is useful when resolving multiple
389+
projects from the same organization.
390+
"""
391+
return bool(get_feature(organization, feature_type=TYPE_CNAME))

readthedocs/invitations/tests/test_views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.contrib.auth.models import User
2-
from django.test import TestCase
2+
from django.test import TestCase, override_settings
33
from django.urls import reverse
44
from django.utils import timezone
55
from django_dynamic_fixture import get
@@ -10,6 +10,7 @@
1010
from readthedocs.projects.models import Project
1111

1212

13+
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
1314
class TestViews(TestCase):
1415
def setUp(self):
1516
self.user = get(User)

0 commit comments

Comments
 (0)