diff --git a/readthedocs-common b/readthedocs-common index 9d766e1c076..4c30188da8a 160000 --- a/readthedocs-common +++ b/readthedocs-common @@ -1 +1 @@ -Subproject commit 9d766e1c076140368baf1c706ef1335ba51c7a17 +Subproject commit 4c30188da8a6763a9c99ce823d44c776875a60c2 diff --git a/readthedocs/core/resolver.py b/readthedocs/core/resolver.py index 9abf9542bbc..4e9b9a0b0b9 100644 --- a/readthedocs/core/resolver.py +++ b/readthedocs/core/resolver.py @@ -81,10 +81,7 @@ def resolve_path(self, project, filename='', version_slug=None, language=None, single_version=None, subdomain=None, cname=None, private=None): """Resolve a URL with a subset of fields defined.""" - relation = project.superprojects.first() cname = cname or project.domains.filter(canonical=True).first() - main_language_project = project.main_language_project - version_slug = version_slug or project.get_default_version() language = language or project.language @@ -93,17 +90,27 @@ def resolve_path(self, project, filename='', version_slug=None, filename = self._fix_filename(project, filename) - if main_language_project: - project_slug = main_language_project.slug - language = project.language - subproject_slug = None - elif relation: - project_slug = relation.parent.slug - subproject_slug = relation.alias - cname = relation.parent.domains.filter(canonical=True).first() - else: - project_slug = project.slug - subproject_slug = None + current_project = project + project_slug = project.slug + subproject_slug = None + # We currently support more than 2 levels of nesting subprojects and + # translations, only loop twice to avoid sticking in the loop + for _ in range(0, 2): + main_language_project = current_project.main_language_project + relation = current_project.superprojects.first() + + if main_language_project: + current_project = main_language_project + project_slug = main_language_project.slug + language = project.language + subproject_slug = None + elif relation: + current_project = relation.parent + project_slug = relation.parent.slug + subproject_slug = relation.alias + cname = relation.parent.domains.filter(canonical=True).first() + else: + break single_version = bool(project.single_version or single_version) @@ -146,17 +153,19 @@ def resolve(self, project, protocol='http', filename='', private=None, def _get_canonical_project(self, project): """ - Get canonical project in the case of subproject or translations. + Recursively get canonical project for subproject or translations. + + We need to recursively search here as a nested translations inside + subprojects, and vice versa, are supported. :type project: Project :rtype: Project """ - main_language_project = project.main_language_project relation = project.superprojects.first() - if main_language_project: - return main_language_project + if project.main_language_project: + return self._get_canonical_project(project.main_language_project) elif relation: - return relation.parent + return self._get_canonical_project(relation.parent) return project def _get_project_subdomain(self, project): diff --git a/readthedocs/rtd_tests/tests/test_resolver.py b/readthedocs/rtd_tests/tests/test_resolver.py index 9f1f2bc36f9..28e415c2072 100644 --- a/readthedocs/rtd_tests/tests/test_resolver.py +++ b/readthedocs/rtd_tests/tests/test_resolver.py @@ -1,15 +1,15 @@ -from __future__ import absolute_import -import mock +# -*- coding: utf-8 -*- +from __future__ import ( + absolute_import, division, print_function, unicode_literals) -from django.test import TestCase -from django.test.utils import override_settings +import django_dynamic_fixture as fixture +import mock +from django.test import TestCase, override_settings +from readthedocs.core.resolver import resolve, resolve_domain, resolve_path from readthedocs.projects.constants import PRIVATE -from readthedocs.projects.models import Project, Domain +from readthedocs.projects.models import Domain, Project, ProjectRelationship from readthedocs.rtd_tests.utils import create_user -from readthedocs.core.resolver import resolve_path, resolve, resolve_domain - -from django_dynamic_fixture import get @override_settings(PUBLIC_DOMAIN='readthedocs.org') @@ -19,9 +19,26 @@ def setUp(self): with mock.patch('readthedocs.projects.models.broadcast'): self.owner = create_user(username='owner', password='test') self.tester = create_user(username='tester', password='test') - self.pip = get(Project, slug='pip', users=[self.owner], main_language_project=None) - self.subproject = get(Project, slug='sub', language='ja', users=[self.owner], main_language_project=None) - self.translation = get(Project, slug='trans', language='ja', users=[self.owner], main_language_project=None) + self.pip = fixture.get( + Project, + slug='pip', + users=[self.owner], + main_language_project=None, + ) + self.subproject = fixture.get( + Project, + slug='sub', + language='ja', + users=[self.owner], + main_language_project=None, + ) + self.translation = fixture.get( + Project, + slug='trans', + language='ja', + users=[self.owner], + main_language_project=None, + ) self.pip.add_subproject(self.subproject) self.pip.translations.add(self.translation) @@ -47,15 +64,18 @@ def test_resolver_filename_index(self): with override_settings(USE_SUBDOMAIN=False): url = resolve_path(project=self.pip, filename='foo/bar/index.html') self.assertEqual(url, '/docs/pip/en/latest/foo/bar/') - url = resolve_path(project=self.pip, filename='foo/index/index.html') + url = resolve_path( + project=self.pip, filename='foo/index/index.html') self.assertEqual(url, '/docs/pip/en/latest/foo/index/') def test_resolver_filename_false_index(self): with override_settings(USE_SUBDOMAIN=False): url = resolve_path(project=self.pip, filename='foo/foo_index.html') self.assertEqual(url, '/docs/pip/en/latest/foo/foo_index.html') - url = resolve_path(project=self.pip, filename='foo_index/foo_index.html') - self.assertEqual(url, '/docs/pip/en/latest/foo_index/foo_index.html') + url = resolve_path( + project=self.pip, filename='foo_index/foo_index.html') + self.assertEqual( + url, '/docs/pip/en/latest/foo_index/foo_index.html') def test_resolver_filename_sphinx(self): self.pip.documentation_type = 'sphinx' @@ -80,7 +100,12 @@ def test_resolver_subdomain(self): self.assertEqual(url, '/en/latest/') def test_resolver_domain_object(self): - self.domain = get(Domain, domain='http://docs.foobar.com', project=self.pip, canonical=True) + self.domain = fixture.get( + Domain, + domain='http://docs.foobar.com', + project=self.pip, + canonical=True, + ) with override_settings(USE_SUBDOMAIN=False): url = resolve_path(project=self.pip, filename='index.html') self.assertEqual(url, '/en/latest/') @@ -89,7 +114,12 @@ def test_resolver_domain_object(self): self.assertEqual(url, '/en/latest/') def test_resolver_domain_object_not_canonical(self): - self.domain = get(Domain, domain='http://docs.foobar.com', project=self.pip, canonical=False) + self.domain = fixture.get( + Domain, + domain='http://docs.foobar.com', + project=self.pip, + canonical=False, + ) with override_settings(USE_SUBDOMAIN=False): url = resolve_path(project=self.pip, filename='') self.assertEqual(url, '/docs/pip/en/latest/') @@ -135,72 +165,94 @@ def test_resolver_translation(self): class ResolverPathOverrideTests(ResolverBase): - """Tests to make sure we can override resolve_path correctly""" + """Tests to make sure we can override resolve_path correctly.""" def test_resolver_force_single_version(self): self.pip.single_version = False with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', single_version=True) + url = resolve_path( + project=self.pip, filename='index.html', single_version=True) self.assertEqual(url, '/docs/pip/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', single_version=True) + url = resolve_path( + project=self.pip, filename='index.html', single_version=True) self.assertEqual(url, '/') def test_resolver_force_domain(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', cname=True) + url = resolve_path( + project=self.pip, filename='index.html', cname=True) self.assertEqual(url, '/en/latest/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', cname=True) + url = resolve_path( + project=self.pip, filename='index.html', cname=True) self.assertEqual(url, '/en/latest/') def test_resolver_force_domain_single_version(self): self.pip.single_version = False with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', single_version=True, cname=True) + url = resolve_path( + project=self.pip, filename='index.html', single_version=True, + cname=True) self.assertEqual(url, '/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', single_version=True, cname=True) + url = resolve_path( + project=self.pip, filename='index.html', single_version=True, + cname=True) self.assertEqual(url, '/') def test_resolver_force_language(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', language='cz') + url = resolve_path( + project=self.pip, filename='index.html', language='cz') self.assertEqual(url, '/docs/pip/cz/latest/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', language='cz') + url = resolve_path( + project=self.pip, filename='index.html', language='cz') self.assertEqual(url, '/cz/latest/') def test_resolver_force_version(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', version_slug='foo') + url = resolve_path( + project=self.pip, filename='index.html', version_slug='foo') self.assertEqual(url, '/docs/pip/en/foo/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', version_slug='foo') + url = resolve_path( + project=self.pip, filename='index.html', version_slug='foo') self.assertEqual(url, '/en/foo/') def test_resolver_force_language_version(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.pip, filename='index.html', language='cz', version_slug='foo') + url = resolve_path( + project=self.pip, filename='index.html', language='cz', + version_slug='foo') self.assertEqual(url, '/docs/pip/cz/foo/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.pip, filename='index.html', language='cz', version_slug='foo') + url = resolve_path( + project=self.pip, filename='index.html', language='cz', + version_slug='foo') self.assertEqual(url, '/cz/foo/') def test_resolver_no_force_translation(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.translation, filename='index.html', language='cz') + url = resolve_path( + project=self.translation, filename='index.html', language='cz') self.assertEqual(url, '/docs/pip/ja/latest/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.translation, filename='index.html', language='cz') + url = resolve_path( + project=self.translation, filename='index.html', language='cz') self.assertEqual(url, '/ja/latest/') def test_resolver_no_force_translation_with_version(self): with override_settings(USE_SUBDOMAIN=False): - url = resolve_path(project=self.translation, filename='index.html', language='cz', version_slug='foo') + url = resolve_path( + project=self.translation, filename='index.html', language='cz', + version_slug='foo') self.assertEqual(url, '/docs/pip/ja/foo/') with override_settings(USE_SUBDOMAIN=True): - url = resolve_path(project=self.translation, filename='index.html', language='cz', version_slug='foo') + url = resolve_path( + project=self.translation, filename='index.html', language='cz', + version_slug='foo') self.assertEqual(url, '/ja/foo/') @@ -217,7 +269,12 @@ def test_domain_resolver(self): @override_settings(PRODUCTION_DOMAIN='readthedocs.org') def test_domain_resolver_with_domain_object(self): - self.domain = get(Domain, domain='docs.foobar.com', project=self.pip, canonical=True) + self.domain = fixture.get( + Domain, + domain='docs.foobar.com', + project=self.pip, + canonical=True, + ) with override_settings(USE_SUBDOMAIN=False): url = resolve_domain(project=self.pip) self.assertEqual(url, 'docs.foobar.com') @@ -243,7 +300,9 @@ def test_domain_resolver_translation(self): url = resolve_domain(project=self.translation) self.assertEqual(url, 'pip.readthedocs.org') - @override_settings(PRODUCTION_DOMAIN='readthedocs.org', PUBLIC_DOMAIN='public.readthedocs.org') + @override_settings( + PRODUCTION_DOMAIN='readthedocs.org', + PUBLIC_DOMAIN='public.readthedocs.org') def test_domain_public(self): with override_settings(USE_SUBDOMAIN=False): url = resolve_domain(project=self.translation) @@ -273,7 +332,12 @@ def test_resolver(self): @override_settings(PRODUCTION_DOMAIN='readthedocs.org') def test_resolver_domain(self): - self.domain = get(Domain, domain='docs.foobar.com', project=self.pip, canonical=True) + self.domain = fixture.get( + Domain, + domain='docs.foobar.com', + project=self.pip, + canonical=True, + ) with override_settings(USE_SUBDOMAIN=False): url = resolve(project=self.pip) self.assertEqual(url, 'http://docs.foobar.com/en/latest/') @@ -285,10 +349,12 @@ def test_resolver_domain(self): def test_resolver_subproject(self): with override_settings(USE_SUBDOMAIN=False): url = resolve(project=self.subproject) - self.assertEqual(url, 'http://readthedocs.org/docs/pip/projects/sub/ja/latest/') + self.assertEqual( + url, 'http://readthedocs.org/docs/pip/projects/sub/ja/latest/') with override_settings(USE_SUBDOMAIN=True): url = resolve(project=self.subproject) - self.assertEqual(url, 'http://pip.readthedocs.org/projects/sub/ja/latest/') + self.assertEqual( + url, 'http://pip.readthedocs.org/projects/sub/ja/latest/') @override_settings(PRODUCTION_DOMAIN='readthedocs.org') def test_resolver_translation(self): @@ -316,10 +382,16 @@ def test_resolver_subproject_alias(self): relation.save() with override_settings(USE_SUBDOMAIN=False): url = resolve(project=self.subproject) - self.assertEqual(url, 'http://readthedocs.org/docs/pip/projects/sub_alias/ja/latest/') + self.assertEqual( + url, + 'http://readthedocs.org/docs/pip/projects/sub_alias/ja/latest/', + ) with override_settings(USE_SUBDOMAIN=True): url = resolve(project=self.subproject) - self.assertEqual(url, 'http://pip.readthedocs.org/projects/sub_alias/ja/latest/') + self.assertEqual( + url, + 'http://pip.readthedocs.org/projects/sub_alias/ja/latest/', + ) @override_settings(PRODUCTION_DOMAIN='readthedocs.org') def test_resolver_private_project(self): @@ -360,7 +432,9 @@ def test_resolver_private_version_override(self): url = resolve(project=self.pip, private=False) self.assertEqual(url, 'http://pip.readthedocs.org/en/latest/') - @override_settings(PRODUCTION_DOMAIN='readthedocs.org', PUBLIC_DOMAIN='public.readthedocs.org') + @override_settings( + PRODUCTION_DOMAIN='readthedocs.org', + PUBLIC_DOMAIN='public.readthedocs.org') def test_resolver_public_domain_overrides(self): with override_settings(USE_SUBDOMAIN=False): url = resolve(project=self.pip, private=True) @@ -369,12 +443,19 @@ def test_resolver_public_domain_overrides(self): self.assertEqual(url, 'http://readthedocs.org/docs/pip/en/latest/') with override_settings(USE_SUBDOMAIN=True): url = resolve(project=self.pip, private=True) - self.assertEqual(url, 'http://pip.public.readthedocs.org/en/latest/') + self.assertEqual( + url, 'http://pip.public.readthedocs.org/en/latest/') url = resolve(project=self.pip, private=False) - self.assertEqual(url, 'http://pip.public.readthedocs.org/en/latest/') + self.assertEqual( + url, 'http://pip.public.readthedocs.org/en/latest/') # Domain overrides PUBLIC_DOMAIN - self.domain = get(Domain, domain='docs.foobar.com', project=self.pip, canonical=True) + self.domain = fixture.get( + Domain, + domain='docs.foobar.com', + project=self.pip, + canonical=True, + ) with override_settings(USE_SUBDOMAIN=True): url = resolve(project=self.pip, private=True) self.assertEqual(url, 'http://docs.foobar.com/en/latest/') @@ -393,10 +474,32 @@ def setUp(self): with mock.patch('readthedocs.projects.models.broadcast'): self.owner = create_user(username='owner', password='test') self.tester = create_user(username='tester', password='test') - self.pip = get(Project, slug='pip', users=[self.owner], main_language_project=None) - self.seed = get(Project, slug='sub', users=[self.owner], main_language_project=None) - self.subproject = get(Project, slug='subproject', language='ja', users=[self.owner], main_language_project=None) - self.translation = get(Project, slug='trans', language='ja', users=[self.owner], main_language_project=None) + self.pip = fixture.get( + Project, + slug='pip', + users=[self.owner], + main_language_project=None, + ) + self.seed = fixture.get( + Project, + slug='sub', + users=[self.owner], + main_language_project=None, + ) + self.subproject = fixture.get( + Project, + slug='subproject', + language='ja', + users=[self.owner], + main_language_project=None, + ) + self.translation = fixture.get( + Project, + slug='trans', + language='ja', + users=[self.owner], + main_language_project=None, + ) self.pip.add_subproject(self.subproject, alias='sub') self.pip.translations.add(self.translation) @@ -414,3 +517,112 @@ class SmartResolverPathTestsAlt(ResolverAltSetUp, SmartResolverPathTests): @override_settings(PUBLIC_DOMAIN='readthedocs.org') class ResolverTestsAlt(ResolverAltSetUp, ResolverTests): pass + + +@override_settings(USE_SUBDOMAIN=True, PUBLIC_DOMAIN='readthedocs.io') +class TestSubprojectsWithTranslations(TestCase): + + def setUp(self): + self.subproject_en = fixture.get( + Project, + language='en', + privacy_level='public', + main_language_project=None, + ) + self.subproject_es = fixture.get( + Project, + language='es', + privacy_level='public', + main_language_project=self.subproject_en, + ) + self.superproject_en = fixture.get( + Project, + language='en', + privacy_level='public', + main_language_project=None, + ) + self.superproject_es = fixture.get( + Project, + language='es', + privacy_level='public', + main_language_project=self.superproject_en, + ) + self.relation = fixture.get( + ProjectRelationship, + parent=self.superproject_en, + child=self.subproject_en, + alias=None, + ) + self.assertIn(self.relation, self.superproject_en.subprojects.all()) + self.assertEqual(self.superproject_en.subprojects.count(), 1) + + def test_subproject_with_translation_without_custom_domain(self): + url = resolve(self.superproject_en, filename='') + self.assertEqual( + url, 'http://{project.slug}.readthedocs.io/en/latest/'.format( + project=self.superproject_en, + )) + + url = resolve(self.superproject_es, filename='') + self.assertEqual( + url, 'http://{project.slug}.readthedocs.io/es/latest/'.format( + project=self.superproject_en, + )) + + url = resolve(self.subproject_en, filename='') + # yapf: disable + self.assertEqual( + url, + ('http://{project.slug}.readthedocs.io/projects/' + '{subproject.slug}/en/latest/').format( + project=self.superproject_en, + subproject=self.subproject_en, + ), + ) + + url = resolve(self.subproject_es, filename='') + self.assertEqual( + url, + ('http://{project.slug}.readthedocs.io/projects/' + '{subproject.slug}/es/latest/').format( + project=self.superproject_en, + subproject=self.subproject_en, + ), + ) + # yapf: enable + + def test_subproject_with_translation_with_custom_domain(self): + fixture.get( + Domain, + domain='docs.example.com', + canonical=True, + cname=True, + https=True, + project=self.superproject_en, + ) + + url = resolve(self.superproject_en, filename='') + self.assertEqual(url, 'http://docs.example.com/en/latest/') + + url = resolve(self.superproject_es, filename='') + self.assertEqual(url, 'http://docs.example.com/es/latest/') + + # yapf: disable + url = resolve(self.subproject_en, filename='') + self.assertEqual( + url, + ('http://docs.example.com/projects/' + '{subproject.slug}/en/latest/').format( + subproject=self.subproject_en, + ), + ) + + url = resolve(self.subproject_es, filename='') + self.assertEqual( + url, + ('http://docs.example.com/projects/' + '{subproject.slug}/es/latest/').format( + subproject=self.subproject_en, + ), + ) + # yapf: enable