Skip to content

Commit fd66a6a

Browse files
committed
Perform redirects at DB level
These queries a little more complex but they allow us to rely on the database to perform the filter and check for all the redirect in a single query than fetching all the instances of the Redirect objects and iterating over them. This will perform only one query to the DB and the result will be the redirect we need to apply or no redirects at all.
1 parent ff0ad67 commit fd66a6a

File tree

2 files changed

+51
-4
lines changed

2 files changed

+51
-4
lines changed

readthedocs/redirects/querysets.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Queryset for the redirects app."""
22

33
from django.db import models
4+
from django.db.models import Value, CharField, Q, F
45

56
from readthedocs.core.utils.extend import SettingsOverrideObject
6-
from readthedocs.projects import constants
77

88

99
class RedirectQuerySetBase(models.QuerySet):
@@ -25,8 +25,55 @@ def api(self, user=None, detail=True):
2525
queryset = self._add_user_repos(queryset, user)
2626
return queryset
2727

28-
def get_redirect_path_with_status(self, path, language=None, version_slug=None):
29-
for redirect in self.select_related('project'):
28+
def get_redirect_path_with_status(self, path, full_path=None, language=None, version_slug=None):
29+
# add extra fields with the ``path`` and ``full_path`` to perform a
30+
# filter at db level instead with Python
31+
queryset = self.annotate(
32+
path=Value(
33+
path,
34+
output_field=CharField(),
35+
),
36+
full_path=Value(
37+
full_path,
38+
output_field=CharField(),
39+
),
40+
)
41+
prefix = Q(
42+
redirect_type='prefix',
43+
path__startswith=F('from_url'),
44+
)
45+
page = Q(
46+
redirect_type='page',
47+
path__iexact=F('from_url'),
48+
)
49+
exact = (
50+
Q(
51+
redirect_type='exact',
52+
from_url__endswith='$rest', # Python implementation does "in"
53+
) | Q(
54+
redirect_type='exact',
55+
full_path__iexact=F('from_url'),
56+
)
57+
)
58+
sphinx_html = (
59+
Q(
60+
redirect_type='sphinx_html',
61+
path__endswith='/',
62+
) | Q(
63+
redirect_type='sphinx_html',
64+
path__endswith='/index.html',
65+
)
66+
)
67+
sphinx_htmldir = Q(
68+
redirect_type='sphinx_html',
69+
path__endswith='.html',
70+
)
71+
72+
# There should be one and only one redirect returned by this query. I
73+
# can't think in a case where there can be more at this point. I'm
74+
# leaving the loop just in case for now
75+
queryset = queryset.filter(prefix | page | exact | sphinx_html | sphinx_htmldir)
76+
for redirect in queryset.select_related('project'):
3077
new_path = redirect.get_redirect_path(
3178
path=path,
3279
language=language,

readthedocs/redirects/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def get_redirect_response(request, full_path):
8484
language, version_slug, path = language_and_version_from_path(path)
8585

8686
path, http_status = project.redirects.get_redirect_path_with_status(
87-
path=path, language=language, version_slug=version_slug
87+
path=path, full_path=full_path, language=language, version_slug=version_slug
8888
)
8989

9090
if path is None:

0 commit comments

Comments
 (0)