Skip to content

Commit bd03f82

Browse files
committed
PR previews: Warn users when enabling the feature on incompatible projects
Closes #9176
1 parent 62effc7 commit bd03f82

File tree

4 files changed

+168
-2
lines changed

4 files changed

+168
-2
lines changed

docs/user/integrations.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ GitLab
8585
* Go to the :guilabel:`Settings` > :guilabel:`Webhooks` page for your project
8686
* For **URL**, use the URL of the integration on Read the Docs,
8787
found on the :guilabel:`Admin` > :guilabel:`Integrations` page
88-
* Leave the default **Push events** selected and mark **Tag push events** also
88+
* Leave the default **Push events** selected,
89+
additionally mark **Tag push events** and **Merge request events**.
8990
* Finish by clicking **Add Webhook**
9091

9192
Gitea

readthedocs/projects/forms.py

+63
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.conf import settings
1111
from django.contrib.auth.models import User
1212
from django.template.loader import render_to_string
13+
from django.urls import reverse
1314
from django.utils.translation import gettext_lazy as _
1415

1516
from readthedocs.builds.constants import INTERNAL
@@ -272,6 +273,68 @@ def __init__(self, *args, **kwargs):
272273
else:
273274
self.fields['default_version'].widget.attrs['readonly'] = True
274275

276+
self.setup_external_builds_option()
277+
278+
def setup_external_builds_option(self):
279+
"""Disable the external builds option if the project doesn't have the proper requirements."""
280+
msg = None
281+
url = reverse("projects_integrations", args=[self.instance.slug])
282+
integrations = list(self.instance.integrations.all())
283+
if not self.has_supported_integration(integrations):
284+
msg = _(
285+
f'To build from pull requests you need a GitHub or GitLab <a href="{url}">integration</a>.'
286+
)
287+
elif not self.can_build_external_versions(integrations):
288+
integrations = self.instance.integrations.all()
289+
# If there is only one integration, link directly to it.
290+
if len(integrations) == 1:
291+
url = reverse(
292+
"projects_integrations_detail",
293+
args=[self.instance.slug, integrations[0].pk],
294+
)
295+
msg = _(
296+
f"To build from pull requests your repository's webhook needs to send pull request events. "
297+
f'Try to <a href="{url}">resync your integration</a>.'
298+
)
299+
300+
if msg:
301+
field = self.fields["external_builds_enabled"]
302+
field.disabled = True
303+
field.help_text = msg + " " + field.help_text
304+
305+
def has_supported_integration(self, integrations):
306+
supported_types = {Integration.GITHUB_WEBHOOK, Integration.GITLAB_WEBHOOK}
307+
for integration in integrations:
308+
if integration.integration_type in supported_types:
309+
return True
310+
return False
311+
312+
def can_build_external_versions(self, integrations):
313+
"""
314+
Check if external versions can be enabled for this project.
315+
316+
A project can build external versions if:
317+
318+
- They are using GitHub or GitLab.
319+
- The repository's webhook is setup to send pull request events.
320+
321+
If the integration's provider data isn't set,
322+
it could mean that the user created the integration manually,
323+
and doesn't have an account connected.
324+
So we don't know for sure if the webhook is sending pull request events.
325+
"""
326+
for integration in integrations:
327+
provider_data = integration.provider_data
328+
if integration.integration_type == Integration.GITHUB_WEBHOOK and (
329+
not provider_data or "pull_request" in provider_data.get("events", [])
330+
):
331+
return True
332+
if integration.integration_type == Integration.GITLAB_WEBHOOK and (
333+
not provider_data or provider_data.get("merge_requests_events")
334+
):
335+
return True
336+
return False
337+
275338
def clean_conf_py_file(self):
276339
filename = self.cleaned_data.get('conf_py_file', '').strip()
277340
if filename and 'conf.py' not in filename:

readthedocs/projects/models.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,9 @@ class Project(models.Model):
218218
external_builds_enabled = models.BooleanField(
219219
_('Build pull requests for this project'),
220220
default=False,
221-
help_text=_('More information in <a href="https://docs.readthedocs.io/page/guides/autobuild-docs-for-pull-requests.html">our docs</a>') # noqa
221+
help_text=_(
222+
'More information in <a href="https://docs.readthedocs.io/page/guides/autobuild-docs-for-pull-requests.html">our docs</a>.' # noqa
223+
),
222224
)
223225
external_builds_privacy_level = models.CharField(
224226
_('Privacy level of Pull Requests'),
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from django.contrib.auth.models import User
2+
from django.test import TestCase
3+
from django.urls import reverse
4+
from django_dynamic_fixture import get
5+
6+
from readthedocs.integrations.models import Integration
7+
from readthedocs.projects.models import Project
8+
9+
10+
class TestExternalBuildOption(TestCase):
11+
def setUp(self):
12+
self.user = get(User)
13+
self.project = get(Project, users=[self.user])
14+
self.integration = get(
15+
Integration,
16+
integration_type=Integration.GITHUB_WEBHOOK,
17+
project=self.project,
18+
)
19+
self.url = reverse("projects_advanced", args=[self.project.slug])
20+
self.client.force_login(self.user)
21+
22+
def test_unsuported_integration(self):
23+
self.integration.delete()
24+
resp = self.client.get(self.url)
25+
field = resp.context["form"].fields["external_builds_enabled"]
26+
self.assertTrue(field.disabled)
27+
self.assertTrue(
28+
field.help_text.startswith(
29+
"To build from pull requests you need a GitHub or GitLab"
30+
)
31+
)
32+
33+
get(
34+
Integration,
35+
project=self.project,
36+
integration_type=Integration.BITBUCKET_WEBHOOK,
37+
)
38+
resp = self.client.get(self.url)
39+
field = resp.context["form"].fields["external_builds_enabled"]
40+
self.assertTrue(field.disabled)
41+
self.assertTrue(
42+
field.help_text.startswith(
43+
"To build from pull requests you need a GitHub or GitLab"
44+
)
45+
)
46+
47+
def test_github_integration(self):
48+
self.integration.provider_data = {}
49+
self.integration.save()
50+
51+
resp = self.client.get(self.url)
52+
field = resp.context["form"].fields["external_builds_enabled"]
53+
self.assertFalse(field.disabled)
54+
self.assertTrue(field.help_text.startswith("More information in"))
55+
56+
self.integration.provider_data = {"events": ["pull_request"]}
57+
self.integration.save()
58+
resp = self.client.get(self.url)
59+
field = resp.context["form"].fields["external_builds_enabled"]
60+
self.assertFalse(field.disabled)
61+
self.assertTrue(field.help_text.startswith("More information in"))
62+
63+
self.integration.provider_data = {"events": []}
64+
self.integration.save()
65+
resp = self.client.get(self.url)
66+
field = resp.context["form"].fields["external_builds_enabled"]
67+
self.assertTrue(field.disabled)
68+
self.assertTrue(
69+
field.help_text.startswith(
70+
"To build from pull requests your repository's webhook needs to send pull request events."
71+
)
72+
)
73+
74+
def test_gitlab_integration(self):
75+
self.integration.integration_type = Integration.GITLAB_WEBHOOK
76+
self.integration.provider_data = {}
77+
self.integration.save()
78+
79+
resp = self.client.get(self.url)
80+
field = resp.context["form"].fields["external_builds_enabled"]
81+
self.assertFalse(field.disabled)
82+
self.assertTrue(field.help_text.startswith("More information in"))
83+
84+
self.integration.provider_data = {"merge_requests_events": True}
85+
self.integration.save()
86+
resp = self.client.get(self.url)
87+
field = resp.context["form"].fields["external_builds_enabled"]
88+
self.assertFalse(field.disabled)
89+
self.assertTrue(field.help_text.startswith("More information in"))
90+
91+
self.integration.provider_data = {"merge_requests_events": False}
92+
self.integration.save()
93+
resp = self.client.get(self.url)
94+
field = resp.context["form"].fields["external_builds_enabled"]
95+
self.assertTrue(field.disabled)
96+
self.assertTrue(
97+
field.help_text.startswith(
98+
"To build from pull requests your repository's webhook needs to send pull request events."
99+
)
100+
)

0 commit comments

Comments
 (0)