March 16, 2022

Enhancing the Django “redirects” app

Rather hidden away in Django’s meanders resides the “redirects” app. Let’s take it for a spin and find out how to extend it even further to accommodate features content managers and marketers use and love.

Default installation and features

The installation procedure from the official documentation says:

Add:

  • django.contrib.sites and django.contrib.redirects (this one possibly as the last entry) to your INSTALLED_APPS.
  • django.contrib.redirects.middleware.RedirectFallbackMiddleware to your MIDDLEWARE setting.

Just run manage.py migrate.

This will add the “Sites” and “Redirects” admin views for performing visual CRUD operations on Redirect instances. Upon clicking to add a new redirection, you will be able to select a site to apply the redirection to, a source (which must return 404, else the redirection won’t work) and a destination. The latter fields can be either absolute or relative URLs.

The redirection thus saved is of a “301” type, by default. That is, a request to the URL will be interpreted as a permanent redirection.

Also, leaving the destination URL blank will result in returning a 410 (“Gone”) status code, no matter what.

Let it handle 302 redirections, instead

Django allows for an easy subclassing of this particular middleware, should you want to change its behaviour to handle “302” (temporary) redirects:

from django.contrib.redirects.middleware import RedirectFallbackMiddleware as BaseRedirectFallbackMiddleware
from django.http import HttpResponseRedirect

class RedirectFallbackMiddleware(BaseRedirectFallbackMiddleware):
    """A middleware handling 302 redirects."""

    response_redirect_class = HttpResponseRedirect

Let it handle both redirect types

The way we’ve operated so far does not account for all the needs contents managers have, so let’s give ’em more control.

Let’s start with subclassing the original model, adding a field accepting integers and the two redirection choices as (status_code: int, label: str) tuples:

from django.contrib.redirects.models import Redirect as BaseRedirect
from django.db import models

class Redirect(BaseRedirect):
    """A redirect."""

    class RedirectTypes(models.IntegerChoices):
        """Redirection types."""
    
        TEMPORARY = 302, "Temporary"
        PERMANENT = 301, "Permanent"
    
    redirect_type = models.IntegerField(
        choices=RedirectTypes.choices
    )

Let’s then go ahead and register a new admin tied with our Redirect model, instead of the default one:

from django.contrib import admin
from django.contrib.redirects.admin import RedirectAdmin as BaseRedirectAdmin
from django.contrib.redirects.models import Redirect as BaseRedirect

class RedirectAdmin(BaseRedirectAdmin):
    """The redirect admin."""
    
admin.site.unregister(BaseRedirect)
admin.site.register(Redirect, RedirectAdmin)

Finally, let’s implement the middleware to take the switch into account. Also, as the original featured one try... except block too many, let’s make the response processing simpler and more readable.

from django.conf import settings
from django.contrib.redirects.middleware import (
RedirectFallbackMiddleware as BaseRedirectFallbackMiddleware,
)
from django.contrib.sites.shortcuts import get_current_site
from django.http import HttpResponsePermanentRedirect, HttpResponseRedirect
from pathlib import Path
from resources.models import Redirect

class RedirectFallbackMiddleware(BaseRedirectFallbackMiddleware):
    """A middleware for handling both 301 and 302 redirections."""

    def process_response(self, request, response):
        """Process the response."""        
        if response.status_code != 404:
            return response
        append_slash = (
            settings.APPEND_SLASH and 
            not Path(request.path).suffix and
            not request.path.endswith("/")
        )
        full_path = (
            request.get_full_path(force_append_slash=append_slash)
        )
        current_site = get_current_site(request)
        try:
            r = Redirect.objects.get(
                site=current_site, old_path=full_path
            )
        except Redirect.DoesNotExist:
            pass
        else:
            redirect_types = {
                301: HttpResponsePermanentRedirect,
                302: HttpResponseRedirect,
            }
            self.response_redirect_class = (
                redirect_types[r.redirect_type]
            )
            if r.new_path == "":
                return self.response_gone_class()
            return self.response_redirect_class(r.new_path)
        return response

What it does is still ensuring that the request comes from a “404” source URL, checking that a redirection exists in the db and, if so, assigning the response class depending on that very redirection’s type selected in the admin, while still accounting for the 410 status code mentioned above.

Copyright © 2024 Niccolò Mineo
Some rights reserved: CC BY-NC 4.0