Skip to content

Commit ed00aed

Browse files
authored
Domains: put a limit of 2 custom domains per project (#11629)
* Domains: put a limit of 2 custom domains per project Related #1808 * Domains: add tests for limit amount of domains * Add missing import * Test: define 2 domains as limit for all the tests
1 parent 9510a69 commit ed00aed

File tree

10 files changed

+89
-7
lines changed

10 files changed

+89
-7
lines changed

readthedocs/domains/validators.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from django.conf import settings
2+
from django.core.exceptions import ValidationError
3+
from django.utils.translation import gettext_lazy as _
4+
5+
from readthedocs.subscriptions.constants import TYPE_CNAME
6+
from readthedocs.subscriptions.products import get_feature
7+
8+
9+
def check_domains_limit(project, error_class=ValidationError):
10+
"""Check if the project has reached the limit on the number of domains."""
11+
feature = get_feature(project, TYPE_CNAME)
12+
if feature.unlimited:
13+
return
14+
15+
if project.domains.count() >= feature.value:
16+
msg = _(
17+
f"This project has reached the limit of {feature.value} domains."
18+
" Consider removing unused domains."
19+
)
20+
if settings.ALLOW_PRIVATE_REPOS:
21+
msg = _(
22+
f"Your organization has reached the limit of {feature.value} domains."
23+
" Consider removing unused domains or upgrading your plan."
24+
)
25+
raise error_class(msg)

readthedocs/projects/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from readthedocs.core.utils import extract_valid_attributes_for_model, slugify
3737
from readthedocs.core.utils.url import unsafe_join_url_path
3838
from readthedocs.domains.querysets import DomainQueryset
39+
from readthedocs.domains.validators import check_domains_limit
3940
from readthedocs.notifications.models import Notification as NewNotification
4041
from readthedocs.projects import constants
4142
from readthedocs.projects.exceptions import ProjectConfigurationError
@@ -1809,6 +1810,9 @@ def restart_validation_process(self):
18091810
self.validation_process_start = timezone.now()
18101811
self.save()
18111812

1813+
def clean(self):
1814+
check_domains_limit(self.project)
1815+
18121816
def save(self, *args, **kwargs):
18131817
parsed = urlparse(self.domain)
18141818
if parsed.scheme or parsed.netloc:

readthedocs/projects/tests/test_domain_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
@override_settings(
1414
RTD_ALLOW_ORGANIZATIONS=False,
15-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
15+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
1616
)
1717
class TestDomainViews(TestCase):
1818
def setUp(self):

readthedocs/proxito/tests/test_full.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ def test_404_download(self):
17251725
ALLOW_PRIVATE_REPOS=True,
17261726
PUBLIC_DOMAIN="dev.readthedocs.io",
17271727
PUBLIC_DOMAIN_USES_HTTPS=True,
1728-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
1728+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
17291729
)
17301730
# We are overriding the storage class instead of using RTD_BUILD_MEDIA_STORAGE,
17311731
# since the setting is evaluated just once (first test to use the storage

readthedocs/proxito/tests/test_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
@pytest.mark.proxito
2222
@override_settings(
2323
PUBLIC_DOMAIN="dev.readthedocs.io",
24-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
24+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
2525
)
2626
class MiddlewareTests(RequestFactoryTestMixin, TestCase):
2727
def setUp(self):

readthedocs/proxito/tests/test_redirects.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
PUBLIC_DOMAIN="dev.readthedocs.io",
1616
RTD_EXTERNAL_VERSION_DOMAIN="dev.readthedocs.build",
1717
PUBLIC_DOMAIN_USES_HTTPS=True,
18-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
18+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
1919
)
2020
class RedirectTests(BaseDocServing):
2121
def test_root_url_no_slash(self):

readthedocs/rtd_tests/tests/test_domains.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from readthedocs.projects.forms import DomainForm
66
from readthedocs.projects.models import Domain, Project
7+
from readthedocs.subscriptions.constants import TYPE_CNAME
8+
from readthedocs.subscriptions.products import RTDProductFeature, get_feature
79

810

911
class ModelTests(TestCase):
@@ -205,3 +207,53 @@ def test_dont_allow_changin_https_to_http(self):
205207
self.assertTrue(form.is_valid())
206208
domain = form.save()
207209
self.assertTrue(domain.https)
210+
211+
@override_settings(
212+
RTD_DEFAULT_FEATURES=dict(
213+
[RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]
214+
),
215+
)
216+
def test_domains_limit(self):
217+
feature = get_feature(self.project, TYPE_CNAME)
218+
form = DomainForm(
219+
{
220+
"domain": "docs.user.example.com",
221+
"canonical": True,
222+
},
223+
project=self.project,
224+
)
225+
self.assertTrue(form.is_valid())
226+
form.save()
227+
self.assertEqual(self.project.domains.all().count(), 1)
228+
229+
form = DomainForm(
230+
{
231+
"domain": "docs.dev.example.com",
232+
"canonical": False,
233+
},
234+
project=self.project,
235+
)
236+
self.assertTrue(form.is_valid())
237+
form.save()
238+
self.assertEqual(self.project.domains.all().count(), 2)
239+
240+
# Creating the third (3) domain should fail the validation form
241+
form = DomainForm(
242+
{
243+
"domain": "docs.customer.example.com",
244+
"canonical": False,
245+
},
246+
project=self.project,
247+
)
248+
self.assertFalse(form.is_valid())
249+
250+
msg = (
251+
f"This project has reached the limit of {feature.value} domains. "
252+
"Consider removing unused domains."
253+
)
254+
if settings.RTD_ALLOW_ORGANIZATIONS:
255+
msg = (
256+
f"Your organization has reached the limit of {feature.value} domains. "
257+
"Consider removing unused domains or upgrading your plan."
258+
)
259+
self.assertEqual(form.errors["__all__"][0], msg)

readthedocs/rtd_tests/tests/test_footer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ def test_private_highest_version(self):
479479
@pytest.mark.proxito
480480
@override_settings(
481481
PUBLIC_DOMAIN="readthedocs.io",
482-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
482+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
483483
)
484484
class TestFooterPerformance(TestCase):
485485
# The expected number of queries for generating the footer

readthedocs/rtd_tests/tests/test_resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
@override_settings(
2222
PUBLIC_DOMAIN="readthedocs.org",
23-
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
23+
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME, value=2).to_item()]),
2424
)
2525
class ResolverBase(TestCase):
2626
def setUp(self):

readthedocs/settings/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def RTD_DEFAULT_FEATURES(self):
170170

171171
return dict(
172172
(
173-
RTDProductFeature(type=constants.TYPE_CNAME).to_item(),
173+
# Max number of domains allowed per project.
174+
RTDProductFeature(type=constants.TYPE_CNAME, value=2).to_item(),
174175
RTDProductFeature(type=constants.TYPE_EMBED_API).to_item(),
175176
# Retention days for search analytics.
176177
RTDProductFeature(

0 commit comments

Comments
 (0)