Skip to content

Commit 266284c

Browse files
committed
Redirects: allow to redirect even if a page exists
1 parent c3ace24 commit 266284c

File tree

10 files changed

+442
-58
lines changed

10 files changed

+442
-58
lines changed

docs/user/user-defined-redirects.rst

+27-14
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,30 @@ User-defined Redirects
33

44
You can set up redirects for a project in your project dashboard's :guilabel:`Redirects` page.
55

6+
.. contents:: Table of contents
7+
:local:
8+
69
Quick Summary
710
-------------
811

9-
* Log into your readthedocs.org account.
10-
* From your dashboard, select the project on which you wish to add redirects.
11-
* From the project's top navigation bar, select the :guilabel:`Admin` tab.
12+
* Go to the :guilabel:`Admin` tab of your project.
1213
* From the left navigation menu, select :guilabel:`Redirects`.
13-
* In the form box "Redirect Type" select the type of redirect you want. See below for detail.
14-
* Depending on the redirect type you select, enter FROM and/or TO URL as needed.
14+
* In the form box "Redirect Type" select the type of redirect you want.
15+
:ref:`See below <user-defined-redirects:redirect types>` for detail.
16+
* Depending on the redirect type you select, enter ``From URL`` and/or ``To URL`` as needed.
1517
* When finished, click the :guilabel:`Add` button.
1618

1719
Your redirects will be effective immediately.
1820

19-
Limitations
20-
~~~~~~~~~~~
21-
22-
Redirects are only implemented in case of a *404 File Not Found* error.
23-
If you need to redirect a large number of files that still exist,
24-
please reach out to :doc:`/support`.
21+
Features
22+
--------
2523

26-
Page & Exact Redirects can redirect to URLs outside Read the Docs.
27-
Define the `To URL` as the absolute URL you want to redirect to.
24+
- By default, redirects are followed only if the requested page doesn't exist
25+
(*404 File Not Found* error), if you need to apply a redirect for files that exist,
26+
mark the :guilabel:`Force redirect` option.
27+
- :ref:`user-defined-redirects:page redirects` and :ref:`user-defined-redirects:exact redirects`
28+
can redirect to URLs outside Read the Docs,
29+
just include the protocol in ``To URL``, e.g ``https://example.com``.
2830

2931
Redirect Types
3032
--------------
@@ -82,7 +84,7 @@ You would set the following configuration::
8284
Because of this,
8385
the ``/`` at the start of the ``From URL`` doesn't include the ``/$lang/$version`` prefix (e.g.
8486
``/en/latest``), but just the version-specific part of the URL.
85-
If you want to set directs only for some languages or some versions, you should use
87+
If you want to set redirects only for some languages or some versions, you should use
8688
:ref:`user-defined-redirects:exact redirects` with the fully-specified path.
8789

8890
Exact Redirects
@@ -128,6 +130,17 @@ Similarly, if you maintain several branches of your documentation (e.g. ``3.0``
128130
``latest``) and decide to move pages in ``latest`` but not the older branches, you can use
129131
*Exact Redirects* to do so.
130132

133+
You can use an exact redirect to migrate your documentation to another domain,
134+
for example::
135+
136+
Type: Exact Redirect
137+
From URL: /$rest
138+
To URL: https://newdocs.example.com/
139+
Force Redirect: True
140+
141+
Then all pages will redirect to the new domain, for example
142+
``https://docs.example.com/en/latest/install.html`` will redirect to
143+
``https://newdocs.example.com/en/latest/install.html``.
131144

132145
Sphinx Redirects
133146
~~~~~~~~~~~~~~~~

readthedocs/projects/forms.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -585,21 +585,30 @@ class RedirectForm(forms.ModelForm):
585585

586586
class Meta:
587587
model = Redirect
588-
fields = ['redirect_type', 'from_url', 'to_url']
588+
fields = ["redirect_type", "from_url", "to_url", "force"]
589589

590590
def __init__(self, *args, **kwargs):
591591
self.project = kwargs.pop('project', None)
592592
super().__init__(*args, **kwargs)
593593

594+
if self.project.has_feature(Feature.ALLOW_FORCED_REDIRECTS):
595+
# Remove the nullable option from the form.
596+
# TODO: remove after migration.
597+
self.fields["force"].widget = forms.CheckboxInput()
598+
self.fields["force"].empty_value = False
599+
else:
600+
self.fields.pop("force")
601+
594602
def save(self, **_): # pylint: disable=arguments-differ
595603
# TODO this should respect the unused argument `commit`. It's not clear
596604
# why this needs to be a call to `create`, instead of relying on the
597605
# super `save()` call.
598606
redirect = Redirect.objects.create(
599607
project=self.project,
600-
redirect_type=self.cleaned_data['redirect_type'],
601-
from_url=self.cleaned_data['from_url'],
602-
to_url=self.cleaned_data['to_url'],
608+
redirect_type=self.cleaned_data["redirect_type"],
609+
from_url=self.cleaned_data["from_url"],
610+
to_url=self.cleaned_data["to_url"],
611+
force=self.cleaned_data.get("force", False),
603612
)
604613
return redirect
605614

readthedocs/projects/models.py

+5
Original file line numberDiff line numberDiff line change
@@ -1770,6 +1770,7 @@ def add_features(sender, **kwargs):
17701770
CDN_ENABLED = "cdn_enabled"
17711771
DOCKER_GVISOR_RUNTIME = "gvisor_runtime"
17721772
RECORD_404_PAGE_VIEWS = "record_404_page_views"
1773+
ALLOW_FORCED_REDIRECTS = "allow_forced_redirects"
17731774

17741775
# Versions sync related features
17751776
SKIP_SYNC_TAGS = 'skip_sync_tags'
@@ -1861,6 +1862,10 @@ def add_features(sender, **kwargs):
18611862
RECORD_404_PAGE_VIEWS,
18621863
_("Record 404s page views."),
18631864
),
1865+
(
1866+
ALLOW_FORCED_REDIRECTS,
1867+
_("Allow forced redirects."),
1868+
),
18641869

18651870
# Versions sync related features
18661871
(

readthedocs/proxito/tests/test_full.py

+18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from readthedocs.projects.models import Domain, Feature, Project
2828
from readthedocs.proxito.views.mixins import ServeDocsMixin
29+
from readthedocs.redirects.models import Redirect
2930
from readthedocs.rtd_tests.storage import BuildMediaFileSystemStorageTest
3031
from readthedocs.subscriptions.models import Plan, PlanFeature, Subscription
3132

@@ -1223,6 +1224,23 @@ def _test_cache_control_header_project(self, expected_value, host=None):
12231224
self.assertEqual(resp.headers['CDN-Cache-Control'], 'private', url)
12241225
self.assertNotIn('Cache-Tag', resp.headers, url)
12251226

1227+
# Forced redirects will be cached only if the version is public.
1228+
get(
1229+
Redirect,
1230+
project=self.project,
1231+
redirect_type="exact",
1232+
from_url="/en/latest/install.html",
1233+
to_url="/en/latest/tutorial/install.html",
1234+
force=True,
1235+
)
1236+
url = "/en/latest/install.html"
1237+
resp = self.client.get(url, secure=True, HTTP_HOST=host)
1238+
self.assertEqual(
1239+
resp["Location"], f"https://{host}/en/latest/tutorial/install.html", url
1240+
)
1241+
self.assertEqual(resp.headers["CDN-Cache-Control"], expected_value, url)
1242+
self.assertEqual(resp.headers["Cache-Tag"], "project,project:latest", url)
1243+
12261244
def _test_cache_control_header_subproject(self, expected_value, host=None):
12271245
"""
12281246
Test the CDN-Cache-Control header on requests for `self.subproject`.

0 commit comments

Comments
 (0)