Create automatic redirects upon changing an URL dependent field in your Django Models

When the generated URLs of your objects depends on a field in your model that could potentially change (for example a slug field that is generated from a title which your editor just changed), you can get yourself into trouble with Google as your old URL will now return a 404. This posts outlines an approach to dynamically create redirects whenever an objects changes an url dependant field.

Update

This is an old post so keep in mind syntax might have changed since it was first written.

If you have a model in which the get_absolute_url() result depends on a field that is easily edited (such a title or a slug that is editable in the django admin for example), you can easily get yourself into trouble with broken links on Google.

An easy way to offset this risk is to write a simple signal that creates a redirect using django.contrib.redirects every time that editable field changes.

For example, changing the slug on a BlogPost model would result in a new url:

models.py
class BlogPost(models.Model):
  # ...
  slug = models.CharField(unique=true, max_length=128)
 
  @models.permalink
  def get_absolute_url(def):
    return ('blogpost_detail', (), { 'slug' : self.slug })
 
from django.db.models.signals import pre_save
from signals import create_redirect
pre_save.connect(create_redirect, sender=BlogPost, dispatch_uid="001")

so we create a new signal handler in signals.py:

signals.py
from django.contrib.redirects.models import Redirect
from django.contrib.sites.models import Site
 
def create_redirect(sender, instance, **kwargs):
  if sender == BlogPost:
    try:
      o = sender.objects.get(id=instance.id)
      if o.slug != instance.slug:
        old_path = o.get_absolute_url()
        new_path = instance.get_absolute_url()
        # Update any existing redirects that are pointing to the old url
        for redirect in Redirect.objects.filter(new_path=old_path):
          redirect.new_path = new_path
          # If the updated redirect now points to itself, delete it
          # (i.e. slug = A -> slug = B -> slug = A again)
          if redirect.new_path == redirect.old_path:
            redirect.delete()
          else:
            redirect.save()
        # Now add the new redirect
        Redirect.objects.create(site=Site.objects.get_current(), old_path=old_path, new_path=new_path)
    except sender.DoesNotExist:
        pass

so now every time you edit the slug field in your model a redirect will be automatically created.