Skip to content

Commit 47546f5

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

File tree

5 files changed

+198
-2
lines changed

5 files changed

+198
-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

+62
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,67 @@ 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 meet the 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+
# If there is only one integration, link directly to it.
289+
if len(integrations) == 1:
290+
url = reverse(
291+
"projects_integrations_detail",
292+
args=[self.instance.slug, integrations[0].pk],
293+
)
294+
msg = _(
295+
f"To build from pull requests your repository's webhook needs to send pull request events. "
296+
f'Try to <a href="{url}">resync your integration</a>.'
297+
)
298+
299+
if msg:
300+
field = self.fields["external_builds_enabled"]
301+
field.disabled = True
302+
field.help_text = msg + " " + field.help_text
303+
304+
def has_supported_integration(self, integrations):
305+
supported_types = {Integration.GITHUB_WEBHOOK, Integration.GITLAB_WEBHOOK}
306+
for integration in integrations:
307+
if integration.integration_type in supported_types:
308+
return True
309+
return False
310+
311+
def can_build_external_versions(self, integrations):
312+
"""
313+
Check if external versions can be enabled for this project.
314+
315+
A project can build external versions if:
316+
317+
- They are using GitHub or GitLab.
318+
- The repository's webhook is setup to send pull request events.
319+
320+
If the integration's provider data isn't set,
321+
it could mean that the user created the integration manually,
322+
and doesn't have an account connected.
323+
So we don't know for sure if the webhook is sending pull request events.
324+
"""
325+
for integration in integrations:
326+
provider_data = integration.provider_data
327+
if integration.integration_type == Integration.GITHUB_WEBHOOK and (
328+
not provider_data or "pull_request" in provider_data.get("events", [])
329+
):
330+
return True
331+
if integration.integration_type == Integration.GITLAB_WEBHOOK and (
332+
not provider_data or provider_data.get("merge_requests_events")
333+
):
334+
return True
335+
return False
336+
275337
def clean_conf_py_file(self):
276338
filename = self.cleaned_data.get('conf_py_file', '').strip()
277339
if filename and 'conf.py' not in filename:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 3.2.13 on 2022-06-02 17:49
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("projects", "0088_domain_field_edits"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="historicalproject",
15+
name="external_builds_enabled",
16+
field=models.BooleanField(
17+
default=False,
18+
help_text='More information in <a href="https://docs.readthedocs.io/page/guides/autobuild-docs-for-pull-requests.html">our docs</a>.',
19+
verbose_name="Build pull requests for this project",
20+
),
21+
),
22+
migrations.AlterField(
23+
model_name="project",
24+
name="external_builds_enabled",
25+
field=models.BooleanField(
26+
default=False,
27+
help_text='More information in <a href="https://docs.readthedocs.io/page/guides/autobuild-docs-for-pull-requests.html">our docs</a>.',
28+
verbose_name="Build pull requests for this project",
29+
),
30+
),
31+
]

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)