Skip to content

Commit 0beaad6

Browse files
authored
Merge pull request #5779 from saadmk11/build-manager-update
Add Build managers and Update Build Querysets.
2 parents cfd085c + 51188e7 commit 0beaad6

File tree

12 files changed

+218
-20
lines changed

12 files changed

+218
-20
lines changed

readthedocs/api/v3/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ class BuildsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin,
269269
lookup_url_kwarg = 'build_pk'
270270
serializer_class = BuildSerializer
271271
filterset_class = BuildFilter
272-
queryset = Build.objects.all()
272+
queryset = Build.internal.all()
273273
permit_list_expands = [
274274
'config',
275275
]

readthedocs/builds/managers.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
TAG,
2222
EXTERNAL,
2323
)
24-
from .querysets import VersionQuerySet
24+
from .querysets import VersionQuerySet, BuildQuerySet
2525

2626
log = logging.getLogger(__name__)
2727

@@ -122,3 +122,61 @@ class InternalVersionManager(SettingsOverrideObject):
122122

123123
class ExternalVersionManager(SettingsOverrideObject):
124124
_default_class = ExternalVersionManagerBase
125+
126+
127+
class BuildManagerBase(models.Manager):
128+
129+
"""
130+
Build manager for manager only queries.
131+
132+
For creating different Managers.
133+
"""
134+
135+
@classmethod
136+
def from_queryset(cls, queryset_class, class_name=None):
137+
# This is overridden because :py:meth:`models.Manager.from_queryset`
138+
# uses `inspect` to retrieve the class methods, and the proxy class has
139+
# no direct members.
140+
queryset_class = get_override_class(
141+
BuildQuerySet,
142+
BuildQuerySet._default_class, # pylint: disable=protected-access
143+
)
144+
return super().from_queryset(queryset_class, class_name)
145+
146+
147+
class InternalBuildManagerBase(BuildManagerBase):
148+
149+
"""
150+
Build manager that only includes internal version builds.
151+
152+
It will exclude pull request/merge request version builds from the queries
153+
and only include BRANCH, TAG, UNKONWN type Version builds.
154+
"""
155+
156+
def get_queryset(self):
157+
return super().get_queryset().exclude(version__type=EXTERNAL)
158+
159+
160+
class ExternalBuildManagerBase(BuildManagerBase):
161+
162+
"""
163+
Build manager that only includes external version builds.
164+
165+
It will only include pull request/merge request version builds in the queries.
166+
"""
167+
168+
def get_queryset(self):
169+
return super().get_queryset().filter(version__type=EXTERNAL)
170+
171+
172+
class BuildManager(SettingsOverrideObject):
173+
_default_class = BuildManagerBase
174+
_override_setting = 'BUILD_MANAGER'
175+
176+
177+
class InternalBuildManager(SettingsOverrideObject):
178+
_default_class = InternalBuildManagerBase
179+
180+
181+
class ExternalBuildManager(SettingsOverrideObject):
182+
_default_class = ExternalBuildManagerBase

readthedocs/builds/models.py

+19-9
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,25 @@
4646
TAG,
4747
VERSION_TYPES,
4848
)
49-
from .managers import (
49+
from readthedocs.builds.managers import (
5050
VersionManager,
5151
InternalVersionManager,
52-
ExternalVersionManager
52+
ExternalVersionManager,
53+
BuildManager,
54+
InternalBuildManager,
55+
ExternalBuildManager,
5356
)
54-
from .querysets import BuildQuerySet, RelatedBuildQuerySet, VersionQuerySet
55-
from .utils import (
57+
from readthedocs.builds.querysets import (
58+
BuildQuerySet,
59+
RelatedBuildQuerySet,
60+
VersionQuerySet,
61+
)
62+
from readthedocs.builds.utils import (
5663
get_bitbucket_username_repo,
5764
get_github_username_repo,
5865
get_gitlab_username_repo,
5966
)
60-
from .version_slug import VersionSlugField
67+
from readthedocs.builds.version_slug import VersionSlugField
6168

6269

6370
log = logging.getLogger(__name__)
@@ -201,7 +208,7 @@ def config(self):
201208
:rtype: dict
202209
"""
203210
last_build = (
204-
self.builds.filter(
211+
self.builds(manager=INTERNAL).filter(
205212
state='finished',
206213
success=True,
207214
).order_by('-date').first()
@@ -628,9 +635,12 @@ class Build(models.Model):
628635
help_text='Build steps stored outside the database.',
629636
)
630637

631-
# Manager
632-
633-
objects = BuildQuerySet.as_manager()
638+
# Managers
639+
objects = BuildManager.from_queryset(BuildQuerySet)()
640+
# Only include BRANCH, TAG, UNKONWN type Version builds.
641+
internal = InternalBuildManager.from_queryset(BuildQuerySet)()
642+
# Only include EXTERNAL type Version builds.
643+
external = ExternalBuildManager.from_queryset(BuildQuerySet)()
634644

635645
CONFIG_KEY = '__config'
636646

readthedocs/builds/querysets.py

-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ def api(self, user=None, detail=True):
116116

117117
class BuildQuerySet(SettingsOverrideObject):
118118
_default_class = BuildQuerySetBase
119-
_override_setting = 'BUILD_MANAGER'
120119

121120

122121
class RelatedBuildQuerySetBase(models.QuerySet):

readthedocs/builds/views.py

+15
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,21 @@ def post(self, request, project_slug):
8484

8585
class BuildList(BuildBase, BuildTriggerMixin, ListView):
8686

87+
def get_queryset(self):
88+
# this is used to include only internal version
89+
# builds in the build list page
90+
self.project_slug = self.kwargs.get('project_slug', None)
91+
self.project = get_object_or_404(
92+
Project.objects.protected(self.request.user),
93+
slug=self.project_slug,
94+
)
95+
queryset = Build.internal.public(
96+
user=self.request.user,
97+
project=self.project,
98+
).select_related('project', 'version')
99+
100+
return queryset
101+
87102
def get_context_data(self, **kwargs):
88103
context = super().get_context_data(**kwargs)
89104

readthedocs/core/templatetags/privacy_tags.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def get_public_projects(context, user):
2626
viewer=context['request'].user,
2727
).prefetch_latest_build().annotate(
2828
_good_build=Exists(
29-
Build.objects.filter(success=True, project=OuterRef('pk')))
29+
Build.internal.filter(success=True, project=OuterRef('pk')))
3030
)
3131
context['public_projects'] = projects
3232
return ''

readthedocs/projects/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ def has_good_build(self):
768768
# Used for Database optimization.
769769
if hasattr(self, '_good_build'):
770770
return self._good_build
771-
return self.builds.filter(success=True).exists()
771+
return self.builds(manager=INTERNAL).filter(success=True).exists()
772772

773773
@property
774774
def has_versions(self):

readthedocs/projects/querysets.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ def prefetch_latest_build(self):
8585

8686
# Prefetch the latest build for each project.
8787
subquery = Subquery(
88-
Build.objects.filter(
88+
Build.internal.filter(
8989
project=OuterRef('project_id')
9090
).order_by('-date').values_list('id', flat=True)[:1]
9191
)
9292
latest_build = Prefetch(
9393
'builds',
94-
Build.objects.filter(pk__in=subquery),
94+
Build.internal.filter(pk__in=subquery),
9595
to_attr=self.model.LATEST_BUILD_CACHE,
9696
)
9797
return self.prefetch_related(latest_build)

readthedocs/rtd_tests/tests/test_doc_serving.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def test_sitemap_xml(self):
313313
# in language and country value. (zh_CN should be zh-CN)
314314
self.assertContains(response, 'zh-CN')
315315

316-
# PR Versions should not be in the sitemap_xml.
316+
# External Versions should not be in the sitemap_xml.
317317
self.assertNotContains(
318318
response,
319319
self.public.get_docs_url(
@@ -322,6 +322,7 @@ def test_sitemap_xml(self):
322322
private=True,
323323
),
324324
)
325+
325326
# Check if STABLE version has 'priority of 1 and changefreq of weekly.
326327
self.assertEqual(
327328
response.context['versions'][0]['loc'],

readthedocs/rtd_tests/tests/test_managers.py

+89-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django_dynamic_fixture import get
44

55
from readthedocs.builds.constants import EXTERNAL, BRANCH, TAG
6-
from readthedocs.builds.models import Version
6+
from readthedocs.builds.models import Version, Build
77
from readthedocs.projects.constants import PUBLIC, PRIVATE, PROTECTED
88
from readthedocs.projects.models import Project
99

@@ -123,3 +123,91 @@ def test_external_version_manager_with_for_project(self):
123123
self.assertIn(
124124
self.public_external_version, Version.external.for_project(self.pip)
125125
)
126+
127+
128+
class TestBuildManagerBase(TestCase):
129+
130+
fixtures = ['test_data']
131+
132+
def setUp(self):
133+
self.user = User.objects.create(username='test_user', password='test')
134+
self.client.login(username='test_user', password='test')
135+
self.pip = Project.objects.get(slug='pip')
136+
print(self.pip.versions.all())
137+
# Create a External Version and build. ie: pull/merge request Version.
138+
self.external_version = get(
139+
Version,
140+
project=self.pip,
141+
active=True,
142+
type=EXTERNAL,
143+
privacy_level=PUBLIC
144+
)
145+
self.external_version_build = get(
146+
Build,
147+
project=self.pip,
148+
version=self.external_version
149+
)
150+
# Create a Internal Version build.
151+
self.internal_version_build = get(
152+
Build,
153+
project=self.pip,
154+
version=self.pip.versions.get(slug='0.8')
155+
)
156+
157+
self.internal_builds = Build.objects.exclude(version__type=EXTERNAL)
158+
159+
160+
class TestInternalBuildManager(TestBuildManagerBase):
161+
162+
"""
163+
Queries using Internal Manager should only include Internal Version builds.
164+
165+
It will exclude pull/merge request Version builds from the queries
166+
and only include BRANCH, TAG, UNKONWN type Versions.
167+
"""
168+
169+
def test_internal_build_manager_with_all(self):
170+
self.assertNotIn(self.external_version_build, Build.internal.all())
171+
172+
def test_internal_build_manager_with_public(self):
173+
self.assertNotIn(self.external_version_build, Build.internal.public())
174+
175+
def test_internal_build_manager_with_public_with_user_and_project(self):
176+
self.assertNotIn(
177+
self.external_version_build,
178+
Build.internal.public(self.user, self.pip)
179+
)
180+
181+
def test_internal_build_manager_with_api(self):
182+
self.assertNotIn(self.external_version_build, Build.internal.api())
183+
184+
185+
class TestExternalBuildManager(TestBuildManagerBase):
186+
187+
"""
188+
Queries using External Manager should only include External Version builds.
189+
190+
It will only include pull/merge request Version builds in the queries.
191+
"""
192+
193+
def test_external_build_manager_with_all(self):
194+
self.assertNotIn(self.internal_builds, Build.external.all())
195+
self.assertIn(self.external_version_build, Build.external.all())
196+
197+
def test_external_build_manager_with_public(self):
198+
self.assertNotIn(self.internal_builds, Build.external.public())
199+
self.assertIn(self.external_version_build, Build.external.public())
200+
201+
def test_external_build_manager_with_public_with_user_and_project(self):
202+
self.assertNotIn(
203+
self.internal_builds,
204+
Build.external.public(self.user, self.pip)
205+
)
206+
self.assertIn(
207+
self.external_version_build,
208+
Build.external.public(self.user, self.pip)
209+
)
210+
211+
def test_external_build_manager_with_api(self):
212+
self.assertNotIn(self.internal_builds, Build.external.api())
213+
self.assertIn(self.external_version_build, Build.external.api())

readthedocs/rtd_tests/tests/test_project.py

+6
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ def test_update_stable_version_excludes_external_versions(self):
160160
# Test that PR Version is not considered for stable.
161161
self.assertEqual(self.pip.update_stable_version(), None)
162162

163+
def test_has_good_build_excludes_external_versions(self):
164+
# Delete all versions excluding PR Versions.
165+
self.pip.versions.exclude(type=EXTERNAL).delete()
166+
# Test that PR Version is not considered for has_good_build.
167+
self.assertFalse(self.pip.has_good_build)
168+
163169

164170
class TestProjectTranslations(ProjectMixin, TestCase):
165171

readthedocs/rtd_tests/tests/test_views.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from django.urls import reverse
88
from django_dynamic_fixture import get, new
99

10-
from readthedocs.builds.constants import LATEST
11-
from readthedocs.builds.models import Build
10+
from readthedocs.builds.constants import LATEST, EXTERNAL
11+
from readthedocs.builds.models import Build, Version
1212
from readthedocs.core.permissions import AdminPermission
1313
from readthedocs.projects.forms import UpdateProjectForm
1414
from readthedocs.projects.models import HTMLFile, Project
@@ -266,6 +266,7 @@ class BuildViewTests(TestCase):
266266

267267
def setUp(self):
268268
self.client.login(username='eric', password='test')
269+
self.pip = Project.objects.get(slug='pip')
269270

270271
@mock.patch('readthedocs.projects.tasks.update_docs_task')
271272
def test_build_redirect(self, mock):
@@ -276,3 +277,23 @@ def test_build_redirect(self, mock):
276277
r._headers['location'][1],
277278
'/projects/pip/builds/%s/' % build.pk,
278279
)
280+
281+
def test_build_list_does_not_include_external_versions(self):
282+
external_version = get(
283+
Version,
284+
project = self.pip,
285+
active = True,
286+
type = EXTERNAL,
287+
)
288+
external_version_build = get(
289+
Build,
290+
project = self.pip,
291+
version = external_version
292+
)
293+
response = self.client.get(
294+
reverse('builds_project_list', args=[self.pip.slug]),
295+
)
296+
self.assertEqual(response.status_code, 200)
297+
298+
self.assertNotIn(external_version_build, response.context['build_qs'])
299+
self.assertNotIn(external_version_build, response.context['active_builds'])

0 commit comments

Comments
 (0)