Skip to content

Commit 5d4da21

Browse files
authored
Merge pull request #4991 from rtfd/humitos/projects/skip
Skip builds when project is not active
2 parents dfbe6df + a3993bd commit 5d4da21

File tree

9 files changed

+99
-18
lines changed

9 files changed

+99
-18
lines changed

common

readthedocs/builds/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
import logging
1313

1414
from builtins import object
15+
from django.contrib import messages
1516
from django.contrib.auth.decorators import login_required
16-
from django.urls import reverse
1717
from django.http import (
1818
HttpResponseForbidden,
1919
HttpResponsePermanentRedirect,
2020
HttpResponseRedirect,
2121
)
2222
from django.shortcuts import get_object_or_404
23+
from django.urls import reverse
2324
from django.utils.decorators import method_decorator
2425
from django.views.generic import DetailView, ListView
2526

@@ -28,6 +29,7 @@
2829
from readthedocs.core.utils import trigger_build
2930
from readthedocs.projects.models import Project
3031

32+
3133
log = logging.getLogger(__name__)
3234

3335

@@ -63,7 +65,18 @@ def post(self, request, project_slug):
6365
slug=version_slug,
6466
)
6567

66-
_, build = trigger_build(project=project, version=version)
68+
update_docs_task, build = trigger_build(project=project, version=version)
69+
if (update_docs_task, build) == (None, None):
70+
# Build was skipped
71+
messages.add_message(
72+
request,
73+
messages.WARNING,
74+
"This project is currently disabled and can't trigger new builds.",
75+
)
76+
return HttpResponseRedirect(
77+
reverse('builds_project_list', args=[project.slug]),
78+
)
79+
6780
return HttpResponseRedirect(
6881
reverse('builds_detail', args=[project.slug, build.pk]),
6982
)

readthedocs/core/utils/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from readthedocs.builds.constants import LATEST, BUILD_STATE_TRIGGERED
2121
from readthedocs.doc_builder.constants import DOCKER_LIMITS
2222

23+
2324
log = logging.getLogger(__name__)
2425

2526
SYNC_USER = getattr(settings, 'SYNC_USER', getpass.getuser())
@@ -87,18 +88,22 @@ def prepare_build(
8788
:param record: whether or not record the build in a new Build object
8889
:param force: build the HTML documentation even if the files haven't changed
8990
:param immutable: whether or not create an immutable Celery signature
90-
:returns: Celery signature of update_docs_task to be executed
91+
:returns: Celery signature of update_docs_task and Build instance
92+
:rtype: tuple
9193
"""
9294
# Avoid circular import
93-
from readthedocs.projects.tasks import update_docs_task
9495
from readthedocs.builds.models import Build
96+
from readthedocs.projects.models import Project
97+
from readthedocs.projects.tasks import update_docs_task
98+
99+
build = None
95100

96-
if project.skip:
97-
log.info(
98-
'Build not triggered because Project.skip=True: project=%s',
101+
if not Project.objects.is_active(project):
102+
log.warning(
103+
'Build not triggered because Project is not active: project=%s',
99104
project.slug,
100105
)
101-
return None
106+
return (None, None)
102107

103108
if not version:
104109
version = project.versions.get(slug=LATEST)
@@ -158,7 +163,8 @@ def trigger_build(project, version=None, record=True, force=False):
158163
:param version: version of the project to be built. Default: ``latest``
159164
:param record: whether or not record the build in a new Build object
160165
:param force: build the HTML documentation even if the files haven't changed
161-
:returns: A tuple (Celery AsyncResult promise, Task Signature from ``prepare_build``)
166+
:returns: Celery AsyncResult promise and Build instance
167+
:rtype: tuple
162168
"""
163169
update_docs_task, build = prepare_build(
164170
project,
@@ -168,9 +174,9 @@ def trigger_build(project, version=None, record=True, force=False):
168174
immutable=True,
169175
)
170176

171-
if update_docs_task is None:
172-
# Current project is skipped
173-
return None
177+
if (update_docs_task, build) == (None, None):
178+
# Build was skipped
179+
return (None, None)
174180

175181
return (update_docs_task.apply_async(), build)
176182

readthedocs/projects/querysets.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
"""Project model QuerySet classes"""
1+
# -*- coding: utf-8 -*-
2+
"""Project model QuerySet classes."""
23

34
from __future__ import absolute_import
45

56
from django.db import models
67
from django.db.models import Q
78
from guardian.shortcuts import get_objects_for_user
89

9-
from . import constants
1010
from readthedocs.core.utils.extend import SettingsOverrideObject
1111

12+
from . import constants
13+
1214

1315
class ProjectQuerySetBase(models.QuerySet):
1416

@@ -54,6 +56,24 @@ def private(self, user=None):
5456
return self._add_user_repos(queryset, user)
5557
return queryset
5658

59+
def is_active(self, project):
60+
"""
61+
Check if the project is active.
62+
63+
The check consists on,
64+
* the Project shouldn't be marked as skipped.
65+
66+
:param project: project to be checked
67+
:type project: readthedocs.projects.models.Project
68+
69+
:returns: whether or not the project is active
70+
:rtype: bool
71+
"""
72+
if project.skip:
73+
return False
74+
75+
return True
76+
5777
# Aliases
5878

5979
def dashboard(self, user=None):

readthedocs/projects/views/private.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from django.contrib import messages
1717
from django.contrib.auth.decorators import login_required
1818
from django.contrib.auth.models import User
19-
from django.urls import reverse
2019
from django.http import (
2120
Http404,
2221
HttpResponseBadRequest,
@@ -25,6 +24,7 @@
2524
)
2625
from django.middleware.csrf import get_token
2726
from django.shortcuts import get_object_or_404, render
27+
from django.urls import reverse
2828
from django.utils.safestring import mark_safe
2929
from django.utils.translation import ugettext_lazy as _
3030
from django.views.generic import ListView, TemplateView, View
@@ -287,6 +287,9 @@ def done(self, form_list, **kwargs):
287287
def trigger_initial_build(self, project):
288288
"""Trigger initial build."""
289289
update_docs, build = prepare_build(project)
290+
if (update_docs, build) == (None, None):
291+
return None
292+
290293
task_promise = chain(
291294
attach_webhook.si(project.pk, self.request.user.pk),
292295
update_docs,

readthedocs/restapi/views/integrations.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
"""Endpoints integrating with Github, Bitbucket, and other webhooks."""
23

34
from __future__ import (
@@ -13,7 +14,7 @@
1314

1415
import six
1516
from django.shortcuts import get_object_or_404
16-
from rest_framework import permissions
17+
from rest_framework import permissions, status
1718
from rest_framework.exceptions import NotFound, ParseError
1819
from rest_framework.renderers import JSONRenderer
1920
from rest_framework.response import Response
@@ -55,11 +56,14 @@ def post(self, request, project_slug):
5556
"""Set up webhook post view with request and project objects."""
5657
self.request = request
5758
self.project = None
59+
self.data = self.get_data()
5860
try:
5961
self.project = self.get_project(slug=project_slug)
62+
if not Project.objects.is_active(self.project):
63+
resp = {'detail': 'This project is currently disabled'}
64+
return Response(resp, status=status.HTTP_406_NOT_ACCEPTABLE)
6065
except Project.DoesNotExist:
6166
raise NotFound('Project not found')
62-
self.data = self.get_data()
6367
resp = self.handle_webhook()
6468
if resp is None:
6569
log.info('Unhandled webhook event')

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,22 @@ def setUp(self):
862862
},
863863
}
864864

865+
def test_webhook_skipped_project(self, trigger_build):
866+
client = APIClient()
867+
self.project.skip = True
868+
self.project.save()
869+
870+
response = client.post(
871+
'/api/v2/webhook/github/{0}/'.format(
872+
self.project.slug,
873+
),
874+
self.github_payload,
875+
format='json',
876+
)
877+
self.assertDictEqual(response.data, {'detail': 'This project is currently disabled'})
878+
self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE)
879+
self.assertFalse(trigger_build.called)
880+
865881
def test_github_webhook_for_branches(self, trigger_build):
866882
"""GitHub webhook API."""
867883
client = APIClient()

readthedocs/rtd_tests/tests/test_core_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ def setUp(self):
1818
self.project = get(Project, container_time_limit=None)
1919
self.version = get(Version, project=self.project)
2020

21+
@mock.patch('readthedocs.projects.tasks.update_docs_task')
22+
def test_trigger_skipped_project(self, update_docs_task):
23+
self.project.skip = True
24+
self.project.save()
25+
result = trigger_build(
26+
project=self.project,
27+
version=self.version,
28+
)
29+
self.assertEqual(result, (None, None))
30+
self.assertFalse(update_docs_task.signature.called)
31+
self.assertFalse(update_docs_task.signature().apply_async.called)
32+
2133
@mock.patch('readthedocs.projects.tasks.update_docs_task')
2234
def test_trigger_custom_queue(self, update_docs):
2335
"""Use a custom queue when routing the task"""

readthedocs/rtd_tests/tests/test_project_querysets.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ def test_subproject_queryset_as_manager_gets_correct_class(self):
3030
'ManagerFromParentRelatedProjectQuerySetBase'
3131
)
3232

33+
def test_is_active(self):
34+
project = fixture.get(Project, skip=False)
35+
self.assertTrue(Project.objects.is_active(project))
36+
37+
project = fixture.get(Project, skip=True)
38+
self.assertFalse(Project.objects.is_active(project))
39+
3340

3441
class FeatureQuerySetTests(TestCase):
3542

0 commit comments

Comments
 (0)