Any object that can be “browsed” to in a Django application needs a friendly URL. Whether it be a blog post, a calendar entry, and event or a user’s profile, referring to them by their ID or Primary Key is not going to help search engines find you.
Although Django provides an excellent Slug field type, and the admin site can automatically populate the slug field, it falls short on two fronts. First, it doesn’t create a slug for user created objects. Second, it does not enforce unique slugs. This is fine for a generic framework, but if your visitors are creating content that needs a slug, i.e. topics in a forum, or their profile pages, you need to automatically generate slugs.
This snippet of code does just that. It works using a signal, making it reusable and applicable to any model in a Django applications. The only requirement is that the model have a “title” (from which the slug is generated) and a “slug” attribute (to store the slug). The slug must have the keyword, unique=True, and it must raise an IntegrityError when trying to save a non-unique object. This is the default behaviour in Django, so as long as your slug has unique=True, you’re good to go.
Ask for forgiveness, not for permission
The key principle here is the idea of asking for forgiveness, not for permission. What do I mean? Well, it would seem logical in Django to simply slugify the title of an instance, and then try to load an object with that slug. If you can load it, it already exists, therefore try another slug. If it doesn’t load it, then you can use this slug. In other words, our application asks if a slug is already in use before it tries to use it.
In the vast majority of cases, titles are already unique and will not cause clashes, therefore, we’re better off simply slugifying the title then saving it, relying on our model to throw back an exception if we can’t use it. Only when this exception is thrown do we need to worry about changing the slug, thus giving (an admittedly modest) performance gain.
The Code
Dump this code in to any file that makes sense to you. I like to store it somewhere like core.utils, but the point here is that it’s a generic library, so just make sure it’s on the Python Path.
from django.db import IntegrityError
from django.template.defaultfilters import slugify
def find_available_slug(object, instance, slug):
"""
Recursive method that will add underscores to a slug field
until a free value is located
"""
instance.slug = slug
try:
instance.save()
return
except IntegrityError:
slug = '%s_' % slug
find_available_slug(object, instance, slug)
def slug_generator(sender, **kwargs):
""" Generates a unique slug for a node """
instance = kwargs['instance']
if instance.slug is not '':
return
slug = slugify(instance.title)
find_available_slug(sender, instance, slug)
Usage
from django.db.models.signals import post_save
from core.utils import slug_generator
class MyModel(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(blank=True, null=True, unique=True)
post_save.connect(slug_generator, sender=MyModel)
