diff --git a/docs/user/subprojects.rst b/docs/user/subprojects.rst index d734246d001..41753175137 100644 --- a/docs/user/subprojects.rst +++ b/docs/user/subprojects.rst @@ -42,15 +42,10 @@ https://docs.example.com/projects/bar/en/latest/ Custom domain on subprojects ---------------------------- -Adding a custom domain to a subproject is allowed, -but your documentation will always be served from +Adding a custom domain to a subproject is not allowed, +since your documentation will always be served from the domain of the parent project. -For example, if the domain of a parent project is ``https://docs.example.com``, -and you add the ``https://subproject.example.com/`` domain to one of its subprojects, -it will always redirect to the domain of the parent project -``https://docs.example.com/projects/subproject/``. - Search ------ diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 8b6c38795a6..0064e548499 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -725,6 +725,13 @@ def is_subproject(self): """Return whether or not this project is a subproject.""" return self.superprojects.exists() + @property + def superproject(self): + relationship = self.get_parent_relationship() + if relationship: + return relationship.parent + return None + @property def alias(self): """Return the alias (as subproject) if it's a subproject.""" # noqa diff --git a/readthedocs/projects/tests/test_domain_views.py b/readthedocs/projects/tests/test_domain_views.py new file mode 100644 index 00000000000..f5e0a7bbe23 --- /dev/null +++ b/readthedocs/projects/tests/test_domain_views.py @@ -0,0 +1,103 @@ +from django.contrib.auth.models import User +from django.test import TestCase, override_settings +from django.urls import reverse +from django_dynamic_fixture import get + +from readthedocs.organizations.models import Organization +from readthedocs.projects.models import Domain, Project + + +@override_settings(RTD_ALLOW_ORGANIZATIONS=False) +class TestDomainViews(TestCase): + def setUp(self): + self.user = get(User, username="user") + self.project = get(Project, users=[self.user], slug="project") + self.subproject = get(Project, users=[self.user], slug="subproject") + self.project.add_subproject(self.subproject) + self.client.force_login(self.user) + + def test_domain_creation(self): + self.assertEqual(self.project.domains.count(), 0) + + resp = self.client.post( + reverse("projects_domains_create", args=[self.project.slug]), + data={"domain": "test.example.com"}, + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual(self.project.domains.count(), 1) + + domain = self.project.domains.first() + self.assertEqual(domain.domain, "test.example.com") + + def test_domain_deletion(self): + domain = get(Domain, project=self.project, domain="test.example.com") + self.assertEqual(self.project.domains.count(), 1) + + resp = self.client.post( + reverse("projects_domains_delete", args=[self.project.slug, domain.pk]), + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual(self.project.domains.count(), 0) + + def test_domain_edit(self): + domain = get( + Domain, project=self.project, domain="test.example.com", canonical=False + ) + + self.assertEqual(domain.canonical, False) + resp = self.client.post( + reverse("projects_domains_edit", args=[self.project.slug, domain.pk]), + data={"canonical": True}, + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual(self.project.domains.count(), 1) + + domain = self.project.domains.first() + self.assertEqual(domain.domain, "test.example.com") + self.assertEqual(domain.canonical, True) + + def test_adding_domain_on_subproject(self): + self.assertEqual(self.subproject.domains.count(), 0) + + resp = self.client.post( + reverse("projects_domains_create", args=[self.subproject.slug]), + data={"domain": "test.example.com"}, + ) + self.assertEqual(resp.status_code, 401) + self.assertEqual(self.subproject.domains.count(), 0) + + def test_delete_domain_on_subproject(self): + domain = get(Domain, project=self.subproject, domain="test.example.com") + self.assertEqual(self.subproject.domains.count(), 1) + + resp = self.client.post( + reverse("projects_domains_delete", args=[self.subproject.slug, domain.pk]), + ) + self.assertEqual(resp.status_code, 302) + self.assertEqual(self.subproject.domains.count(), 0) + + def test_edit_domain_on_subproject(self): + domain = get( + Domain, project=self.subproject, domain="test.example.com", canonical=False + ) + + self.assertEqual(domain.canonical, False) + resp = self.client.post( + reverse("projects_domains_edit", args=[self.subproject.slug, domain.pk]), + data={"canonical": True}, + ) + self.assertEqual(resp.status_code, 401) + self.assertEqual(self.subproject.domains.count(), 1) + + domain = self.subproject.domains.first() + self.assertEqual(domain.domain, "test.example.com") + self.assertEqual(domain.canonical, False) + + +@override_settings(RTD_ALLOW_ORGANIZATIONS=True) +class TestDomainViewsWithOrganizations(TestDomainViews): + def setUp(self): + super().setUp() + self.org = get( + Organization, owners=[self.user], projects=[self.project, self.subproject] + ) diff --git a/readthedocs/projects/views/base.py b/readthedocs/projects/views/base.py index ba76f7c137f..2ba29212e06 100644 --- a/readthedocs/projects/views/base.py +++ b/readthedocs/projects/views/base.py @@ -1,11 +1,9 @@ """Mix-in classes for project views.""" -import structlog from functools import lru_cache +import structlog from django.conf import settings -from django.http import HttpResponseRedirect -from django.shortcuts import render, get_object_or_404 -from django.urls import reverse +from django.shortcuts import get_object_or_404, render from readthedocs.projects.models import Project @@ -72,7 +70,9 @@ def get_project(self): def get_context_data(self, **kwargs): """Add project to context data.""" context = super().get_context_data(**kwargs) - context['project'] = self.get_project() + project = self.get_project() + context["project"] = project + context["superproject"] = project and project.superproject return context def get_form(self, data=None, files=None, **kwargs): @@ -92,7 +92,9 @@ class ProjectSpamMixin: def get(self, request, *args, **kwargs): if 'readthedocsext.spamfighting' in settings.INSTALLED_APPS: - from readthedocsext.spamfighting.utils import is_show_dashboard_denied # noqa + from readthedocsext.spamfighting.utils import ( # noqa + is_show_dashboard_denied, + ) if is_show_dashboard_denied(self.get_project()): return render(request, template_name='spam.html', status=410) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index e65abe90ce5..0d20044776b 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -435,10 +435,7 @@ def get_success_url(self): class ProjectRelationshipList(ProjectRelationListMixin, ProjectRelationshipMixin, ListView): - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - ctx['superproject'] = self.project.superprojects.first() - return ctx + pass class ProjectRelationshipCreate(ProjectRelationshipMixin, CreateView): @@ -786,7 +783,7 @@ class DomainCreateBase(DomainMixin, CreateView): def post(self, request, *args, **kwargs): project = self.get_project() - if self._is_enabled(project): + if self._is_enabled(project) and not project.superproject: return super().post(request, *args, **kwargs) return HttpResponse('Action not allowed', status=401) @@ -810,7 +807,7 @@ class DomainUpdateBase(DomainMixin, UpdateView): def post(self, request, *args, **kwargs): project = self.get_project() - if self._is_enabled(project): + if self._is_enabled(project) and not project.superproject: return super().post(request, *args, **kwargs) return HttpResponse('Action not allowed', status=401) diff --git a/readthedocs/templates/projects/domain_form.html b/readthedocs/templates/projects/domain_form.html index debe65ef8bc..4c292474a23 100644 --- a/readthedocs/templates/projects/domain_form.html +++ b/readthedocs/templates/projects/domain_form.html @@ -49,7 +49,7 @@ {% endif %}
+ {% if not superproject %} +- {% blocktrans trimmed with project=superproject.parent.name %} + {% blocktrans trimmed with project=superproject.name %} This project is already configured as a subproject of {{ project }}. Nested subprojects are not currently supported. {% endblocktrans %}
- - {% blocktrans trimmed with project=superproject.parent.name %} + + {% blocktrans trimmed with project=superproject.name %} View subprojects of {{ project }} {% endblocktrans %}