Auto generating unique slug in Django: a generic approach

More than a year ago I wrote the post: Auto generating unique slug in Django. There we considered a model named Article, which had title and slug fields. We had overridden the save method of that model to automatically save the unique slug which had been generated by our written _get_unique_slug model method. But what will happen if we want to auto generate unique slug for tens or even hundreds of models? Do the same for each of the models? No way! So, in this post, we will know how we can achieve that through a generic approach. To continue reading this post, you don’t necessarily need to read that previous post, but recommended.

The Basics

What is slug? Consider the URL of this post: /auto-generating-unique-slug-django-generic-approach/ . Here the bold part of the URL is called slug.

Suppose we have tens of models including the Author:

1
2
3
4
5
6
7
8
9
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True)

    def __str__(self):
        return self.name

Based on the name filed, slug filed should be generated. If the name of our first Author is ‘John Doe’, the slug should be ‘john-doe’. If the name of our second Author is also ‘John Doe’, the slug should be ‘john-doe-1’. And if the name of our third Author is ‘John Doe’ again, the slug should be ‘john-doe-2’. And so on.

The Solution

Let’s create a file named utils.py in the app directory and write this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django.utils.text import slugify


def get_unique_slug(model_instance, slugable_field_name, slug_field_name):
    """
    Takes a model instance, sluggable field name (such as 'title') of that
    model as string, slug field name (such as 'slug') of the model as string;
    returns a unique slug as string.
    """
    slug = slugify(getattr(model_instance, slugable_field_name))
    unique_slug = slug
    extension = 1
    ModelClass = model_instance.__class__

    while ModelClass._default_manager.filter(
        **{slug_field_name: unique_slug}
    ).exists():
        unique_slug = '{}-{}'.format(slug, extension)
        extension += 1

    return unique_slug

As the docstring says, the get_unique_slug takes a django model instance for the first argument, sluggable field name such as ’name’ (based on which the slug will be generated) of that model as string for the second argument and slug field name such as ‘slug’ of the model as string for the third argument. And it returns a unique slug as string.

Let’s break down the code a bit. At line 10, slugify (django.utils.text.slugify) takes a string like ‘John Doe’ as argument and returns a string like ‘john-doe’. And we are accessing the sluggable field of the model instance using getattr. At line 13, we are getting the model class from the model instance using class.

Take a closer look at line 15-19. Until we get a unique slug, we append the current slug string with an extension number starting from 1; such as ‘john-doe-1’, ‘john-doe-2’…. For accessing the model manager (usually objects) we are using _default_manager. Line 15 might look a bit tricky, in short, **{slug_field_name: unique_slug} works like something similar to slug='john-doe'. To understand this clearly, we need to know about how **kwargs works in python and how django model unpacks it. As this is beyond the scope of this post, I’m not discussing it here.

Let’s update our models.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.db import models

from .utils import get_unique_slug


class Author(models.Model):
    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = get_unique_slug(self, 'name', 'slug')
        super().save(*args, **kwargs)

    def __str__(self):
        return self.name

Here we are overriding the save method, if the slug is already not set, we are setting it using the get_unique_slug function.

You can also consider to use the pre_save signal instead of overriding the save method.

Boom! all done, enjoy!

Load Comments?