diff --git a/readthedocs/redirects/admin.py b/readthedocs/redirects/admin.py index 4ce0239d48f..aacdda699fd 100644 --- a/readthedocs/redirects/admin.py +++ b/readthedocs/redirects/admin.py @@ -17,6 +17,7 @@ class RedirectAdmin(admin.ModelAdmin): 'from_url', 'to_url', ) + readonly_fields = ('from_url_without_rest',) admin.site.register(Redirect, RedirectAdmin) diff --git a/readthedocs/redirects/migrations/0004_denormalize-from-url.py b/readthedocs/redirects/migrations/0004_denormalize-from-url.py new file mode 100644 index 00000000000..3e61ab3675a --- /dev/null +++ b/readthedocs/redirects/migrations/0004_denormalize-from-url.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2020-03-14 14:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def forward(apps, schema_editor): + """Calculate new ``from_url_without_rest`` attribute.""" + Redirect = apps.get_model('redirects', 'Redirect') + + queryset = Redirect.objects.filter( + redirect_type='exact', + from_url__endswith='$rest', + ) + for redirect in queryset: + redirect.from_url_without_rest = redirect.from_url.replace('$rest', '') or None + redirect.save() + +def backward(apps, schema_editor): + # just no-op + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('redirects', '0003_add_default_redirect_http_status_to_302'), + ] + + operations = [ + migrations.AddField( + model_name='redirect', + name='from_url_without_rest', + field=models.CharField(blank=True, db_index=True, help_text='Only for internal querying use', max_length=255, null=True), + ), + migrations.RunPython(forward, backward), + ] diff --git a/readthedocs/redirects/models.py b/readthedocs/redirects/models.py index 57990ee1772..19562ebf3ab 100644 --- a/readthedocs/redirects/models.py +++ b/readthedocs/redirects/models.py @@ -77,6 +77,16 @@ class Redirect(models.Model): blank=True, ) + # We are denormalizing the database here to easily query for Exact Redirects + # with ``$rest`` on them from El Proxito + from_url_without_rest = models.CharField( + max_length=255, + db_index=True, + help_text='Only for internal querying use', + blank=True, + null=True, + ) + to_url = models.CharField( _('To URL'), max_length=255, @@ -102,6 +112,11 @@ class Meta: verbose_name_plural = _('redirects') ordering = ('-update_dt',) + def save(self, *args, **kwargs): # pylint: disable=arguments-differ + if self.redirect_type == 'exact' and '$rest' in self.from_url: + self.from_url_without_rest = self.from_url.replace('$rest', '') + super().save(*args, **kwargs) + def __str__(self): redirect_text = '{type}: {from_to_url}' if self.redirect_type in ['prefix', 'page', 'exact']: diff --git a/readthedocs/redirects/querysets.py b/readthedocs/redirects/querysets.py index 1d7fb15e7fe..897fb61a182 100644 --- a/readthedocs/redirects/querysets.py +++ b/readthedocs/redirects/querysets.py @@ -1,9 +1,14 @@ """Queryset for the redirects app.""" +import logging + from django.db import models +from django.db.models import Value, CharField, Q, F from readthedocs.core.utils.extend import SettingsOverrideObject +log = logging.getLogger(__name__) + class RedirectQuerySetBase(models.QuerySet): @@ -25,7 +30,56 @@ def api(self, user=None, detail=True): return queryset def get_redirect_path_with_status(self, path, full_path=None, language=None, version_slug=None): - for redirect in self.select_related('project'): + # add extra fields with the ``path`` and ``full_path`` to perform a + # filter at db level instead with Python + queryset = self.annotate( + path=Value( + path, + output_field=CharField(), + ), + full_path=Value( + full_path, + output_field=CharField(), + ), + ) + prefix = Q( + redirect_type='prefix', + path__startswith=F('from_url'), + ) + page = Q( + redirect_type='page', + path__exact=F('from_url'), + ) + exact = ( + Q( + redirect_type='exact', + from_url__endswith='$rest', + full_path__startswith=F('from_url_without_rest'), + ) | Q( + redirect_type='exact', + full_path__exact=F('from_url'), + ) + ) + sphinx_html = ( + Q( + redirect_type='sphinx_html', + path__endswith='/', + ) | Q( + redirect_type='sphinx_html', + path__endswith='/index.html', + ) + ) + sphinx_htmldir = Q( + redirect_type='sphinx_htmldir', + path__endswith='.html', + ) + + queryset = queryset.filter(prefix | page | exact | sphinx_html | sphinx_htmldir) + + # There should be one and only one redirect returned by this query. I + # can't think in a case where there can be more at this point. I'm + # leaving the loop just in case for now + for redirect in queryset.select_related('project'): new_path = redirect.get_redirect_path( path=path, language=language,