Skip to content

Commit c9884a0

Browse files
stsewdbenjaoming
andauthored
Proxito: redirect to default version from root language (#10313)
Closes #2968 --------- Co-authored-by: Benjamin Balder Bach <[email protected]>
1 parent c794ffa commit c9884a0

File tree

5 files changed

+132
-2
lines changed

5 files changed

+132
-2
lines changed

docs/user/user-defined-redirects.rst

+16
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ For example::
8484
You can choose which is the :term:`default version` for Read the Docs to display.
8585
This usually corresponds to the most recent official release from your project.
8686

87+
Root language redirect at ``/<lang>/``
88+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89+
90+
A link to the root language of your documentation (``<slug>.readthedocs.io/en/``)
91+
will redirect to the :term:`default version` of that language.
92+
93+
.. TODO: Remove this once the feature is default on .com
94+
95+
This redirect is currently only active on |org_brand| (``<slug>.readthedocs.io`` and :doc:`custom domains </custom-domains>`).
96+
97+
Root language redirects on |com_brand| can be enabled by contacting :doc:`support </support>`.
98+
99+
For example, accessing the English language of the project will redirect you to the its version (``stable``)::
100+
101+
https://docs.readthedocs.io/en/ -> https://docs.readthedocs.io/en/stable/
102+
87103
Shortlink with ``https://<slug>.rtfd.io``
88104
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89105

readthedocs/core/unresolver.py

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def __init__(self, project, language, version_slug, filename):
6060
self.version_slug = version_slug
6161

6262

63+
class TranslationWithoutVersionError(UnresolverError):
64+
def __init__(self, project, language):
65+
self.project = project
66+
self.language = language
67+
68+
6369
class InvalidPathForVersionedProjectError(UnresolverError):
6470
def __init__(self, project, path):
6571
self.project = project
@@ -272,6 +278,14 @@ def _match_multiversion_project(
272278
filename=file,
273279
)
274280

281+
# If only the language part was given,
282+
# we can't resolve the version.
283+
if version_slug is None:
284+
raise TranslationWithoutVersionError(
285+
project=project,
286+
language=language,
287+
)
288+
275289
if external_version_slug and external_version_slug != version_slug:
276290
raise InvalidExternalVersionError(
277291
project=project,

readthedocs/proxito/tests/test_redirects.py

+49
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from django.test import override_settings
33
from django_dynamic_fixture import get
44

5+
from readthedocs.builds.models import Version
6+
from readthedocs.projects.constants import PUBLIC
57
from readthedocs.projects.models import Feature
68
from readthedocs.proxito.constants import RedirectType
79
from readthedocs.subscriptions.constants import TYPE_CNAME
@@ -11,6 +13,7 @@
1113

1214
@override_settings(
1315
PUBLIC_DOMAIN='dev.readthedocs.io',
16+
RTD_EXTERNAL_VERSION_DOMAIN="dev.readthedocs.build",
1417
PUBLIC_DOMAIN_USES_HTTPS=True,
1518
RTD_DEFAULT_FEATURES={
1619
TYPE_CNAME: 1,
@@ -412,3 +415,49 @@ def setUp(self):
412415
default_true=True,
413416
future_default_true=True,
414417
)
418+
419+
def test_redirect_from_root_language_to_default_version(self):
420+
paths = [
421+
"/en",
422+
"/en/",
423+
]
424+
for path in paths:
425+
r = self.client.get(
426+
path,
427+
secure=True,
428+
HTTP_HOST="project.dev.readthedocs.io",
429+
)
430+
self.assertEqual(r.status_code, 302)
431+
self.assertEqual(
432+
r["Location"],
433+
"https://project.dev.readthedocs.io/en/latest/",
434+
)
435+
self.assertEqual(r.headers["CDN-Cache-Control"], "public")
436+
self.assertEqual(r.headers["Cache-Tag"], "project")
437+
self.assertEqual(r.headers["X-RTD-Redirect"], RedirectType.system.name)
438+
439+
def test_redirect_from_root_language_to_default_external_version(self):
440+
get(
441+
Version,
442+
slug="10",
443+
project=self.project,
444+
privacy_level=PUBLIC,
445+
)
446+
paths = [
447+
"/en",
448+
"/en/",
449+
]
450+
for path in paths:
451+
r = self.client.get(
452+
path,
453+
secure=True,
454+
HTTP_HOST="project--10.dev.readthedocs.build",
455+
)
456+
self.assertEqual(r.status_code, 302)
457+
self.assertEqual(
458+
r["Location"],
459+
"https://project--10.dev.readthedocs.build/en/10/",
460+
)
461+
self.assertEqual(r.headers["CDN-Cache-Control"], "public")
462+
self.assertEqual(r.headers["Cache-Tag"], "project")
463+
self.assertEqual(r.headers["X-RTD-Redirect"], RedirectType.system.name)

readthedocs/proxito/views/serve.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
InvalidExternalVersionError,
2020
InvalidPathForVersionedProjectError,
2121
TranslationNotFoundError,
22+
TranslationWithoutVersionError,
2223
VersionNotFoundError,
2324
unresolver,
2425
)
@@ -357,6 +358,25 @@ def get_using_unresolver(self, request):
357358
# TODO: find a better way to pass this to the middleware.
358359
request.path_project_slug = exc.project.slug
359360
raise Http404
361+
except TranslationWithoutVersionError as exc:
362+
project = exc.project
363+
# TODO: find a better way to pass this to the middleware.
364+
request.path_project_slug = project.slug
365+
366+
if unresolved_domain.is_from_external_domain:
367+
version_slug = unresolved_domain.external_version_slug
368+
else:
369+
version_slug = None
370+
# Redirect to the default version of the current translation.
371+
# This is `/en -> /en/latest/` or
372+
# `/projects/subproject/en/ -> /projects/subproject/en/latest/`.
373+
return self.system_redirect(
374+
request=request,
375+
final_project=project,
376+
version_slug=version_slug,
377+
filename="",
378+
is_external_version=unresolved_domain.is_from_external_domain,
379+
)
360380
except InvalidPathForVersionedProjectError as exc:
361381
project = exc.project
362382
if unresolved_domain.is_from_external_domain:
@@ -745,7 +765,9 @@ def get_using_unresolver(self, request, path):
745765

746766
project = None
747767
version = None
748-
filename = None
768+
# If we weren't able to resolve a filename,
769+
# then the path is the filename.
770+
filename = path
749771
lang_slug = None
750772
version_slug = None
751773
# Try to map the current path to a project/version/filename.
@@ -778,6 +800,10 @@ def get_using_unresolver(self, request, path):
778800
version_slug = exc.version_slug
779801
filename = exc.filename
780802
contextualized_404_class = ProjectTranslationHttp404
803+
except TranslationWithoutVersionError as exc:
804+
project = exc.project
805+
lang_slug = exc.language
806+
# TODO: Use a contextualized 404
781807
except InvalidExternalVersionError as exc:
782808
project = exc.project
783809
# TODO: Use a contextualized 404

readthedocs/rtd_tests/tests/test_unresolver.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
InvalidPathForVersionedProjectError,
1313
SuspiciousHostnameError,
1414
TranslationNotFoundError,
15+
TranslationWithoutVersionError,
1516
VersionNotFoundError,
1617
unresolve,
1718
)
@@ -99,10 +100,34 @@ def test_path_no_version(self):
99100
"https://pip.readthedocs.io/en/",
100101
]
101102
for url in urls:
102-
with pytest.raises(VersionNotFoundError) as excinfo:
103+
with pytest.raises(TranslationWithoutVersionError) as excinfo:
103104
unresolve(url)
104105
exc = excinfo.value
105106
self.assertEqual(exc.project, self.pip)
107+
self.assertEqual(exc.language, "en")
108+
109+
urls = [
110+
"https://pip.readthedocs.io/ja",
111+
"https://pip.readthedocs.io/ja/",
112+
]
113+
for url in urls:
114+
with pytest.raises(TranslationWithoutVersionError) as excinfo:
115+
unresolve(url)
116+
exc = excinfo.value
117+
self.assertEqual(exc.project, self.translation)
118+
self.assertEqual(exc.language, "ja")
119+
120+
def test_invalid_language_no_version(self):
121+
urls = [
122+
"https://pip.readthedocs.io/es",
123+
"https://pip.readthedocs.io/es/",
124+
]
125+
for url in urls:
126+
with pytest.raises(TranslationNotFoundError) as excinfo:
127+
unresolve(url)
128+
exc = excinfo.value
129+
self.assertEqual(exc.project, self.pip)
130+
self.assertEqual(exc.language, "es")
106131
self.assertEqual(exc.version_slug, None)
107132
self.assertEqual(exc.filename, "/")
108133

0 commit comments

Comments
 (0)