Generating slugs automatically in Django without packages - Two easy and solid approaches

Slugs in Django
Image: Slugs in Django (License: CC-BY-SA Marcelo Canina)

Slug generation management

Published:
Last modified:
Tags Django , Slugs , Urls

Overview

One important task when developing a Django website is to have pretty URLs, i.e.: human readable and SEO friendly URLs.

A site's URL structure should be as simple as possible. Consider organizing your content so that URLs are constructed logically and in a manner that is most intelligible to humans (when possible, readable words rather than long ID numbers)

Options

We will explore two approaches to have clean URLs, from having the typical Django’s URLs with the object/pk scheme, like /article/12345 to have one of these:

  1. pk and slugs: object/pk-slug like /article/12345-my-example-title where we add the object’s slug after the primary key.

  2. unique slug: generate a unique slugs without showing the primary key like /article/my-example-title

A slug is a short label for something, containing only letters, numbers, underscores or hyphens. They’re generally used in URLs..

Example app

The following app consisting of an Article model with a title will help to show how to use the pk-slug URL.

Example blog/views.py:

from django.views.generic.detail import DetailView

from blog.models import Article

class ArticleDetailView(DetailView):

    model = Article

Example blog/models.py:

from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=100)

Example djangoslugs/urls.py:

from django.urls import path

from blog.views import ArticleDetailView

urlpatterns = [
   path('blog/<int:pk>/', ArticleDetailView.as_view(), name='article-detail'),
]

Demo and Repo

A working demo is available at https://django-slugs-example-app.herokuapp.com/ that shows how both approaches work.

And its repo at: https://github.com/marcanuy/django-slugs-example-app Star using the above scheme.

First approach: PK and Slug

Pros

This approach increases Web Application security by avoiding an authorization attack: Insecure Direct Object References (IDOR)

Insecure Direct Object References occur when an application provides direct access to objects based on user-supplied input. As a result of this vulnerability attackers can bypass authorization and access resources in the system directly, for example database records or files.

This can be prevented by using both, an object primary key and the slug, as the Simple Object Mixin query_pk_and_slug docs states:

When applications allow access to individual objects by a sequential primary key, an attacker could brute-force guess all URLs; thereby obtaining a list of all objects in the application.

If users with access to individual objects should be prevented from obtaining this list, setting query_pk_and_slug to True will help prevent the guessing of URLs as each URL will require two correct, non-sequential arguments. Simply using a unique slug may serve the same purpose, but this scheme allows you to have non-unique slugs.

Note

Many apps dedicated to manage slugs comes with a max_length restriction for its size and they truncate the string according to that value.

We are gonna use the same length as the slugified field.

1. Use SlugField

There is a special model Field type in Django for slugs: SlugField.

Create a field named slug with type: SlugField.

in blog/models.py:

from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.text import slugify

class ArticlePkAndSlug(models.Model):
    title = models.CharField(
        max_length=settings.BLOG_TITLE_MAX_LENGTH
    )
    slug = models.SlugField(
        default='',
        editable=False,
        max_length=settings.BLOG_TITLE_MAX_LENGTH,
    )

    def get_absolute_url(self):
        kwargs = {
            'pk': self.id,
            'slug': self.slug
        }
        return reverse('article-pk-slug-detail', kwargs=kwargs)

    def save(self, *args, **kwargs):
        value = self.title
        self.slug = slugify(value, allow_unicode=True)
        super().save(*args, **kwargs)

URLs in djangoslugs/urls.py:

    ...
    path('blog/<int:pk>-<str:slug>/', ArticleDetailView.as_view() , name='article-detail')

And in the view: blog/views.py

class Article(DetailView):
    model = Article
    query_pk_and_slug = True

Before saving the instance, we convert the title to a slug with the slugify Django command, that basically replaces spaces by hyphens.

As SlugField inherits from CharField, it comes with the attribute max_length which handles the maximum length of the string it contains at database level.

If we use a SlugField without specifying its max_length attribute, it gets the value of 50 by default, which can lead to problems when we generate the string from a bigger max_length field.

So the trick is to make them both use the same length.

Second approach: unique slugs

In this case title and slug don’t need to have the same max_length, but this brings two issues when generating the slug:

  1. Truncate the slug to respect max_length
  2. Control the slug uniqueness by adding a suffix if another slug with the same string exists1
  3. Avoid generating a new slug if it already has one when saving an existing instance

1. Truncate the slug

Before generating the slug, get the max_length value max_length = self._meta.get_field('slug').max_length and truncate at that position slug = slugify(self.title)[:max_length].

2. Ensure uniqueness

For each slug candidate we make sure it is unique by testing against the database until we have a non existing one.

import itertools

...

slug_candidate = slug_original = slugify(self.title)

for i in itertools.count(1):
	if not Article.objects.filter(slug=slug_candidate).exists():
		break
	slug_candidate = '{}-{}'.format(slug_original, i)

3. Avoid regenerating slug

To avoid generating the slug each time you save an existing model, detect if it is an update of the model on each call of model’s save() method.

def _generate_slug(self):
	...

def save(self, *args, **kwargs):
	if not self.pk:
		self._generate_slug()

	super().save(*args, **kwargs)

All together

In save method:

import itertools
...

class ArticleUniqueSlug(Article):

    def _generate_slug(self):
        max_length = self._meta.get_field('slug').max_length
        value = self.title
        slug_candidate = slug_original = slugify(value, allow_unicode=True)
        for i in itertools.count(1):
            if not ArticleUniqueSlug.objects.filter(slug=slug_candidate).exists():
                break
            slug_candidate = '{}-{}'.format(slug_original, i)

        self.slug = slug_candidate

    def save(self, *args, **kwargs):
        if not self.pk:
            self._generate_slug()

        super().save(*args, **kwargs)

In view blog/views.py:

class Article(DetailView):
    model = ArticlePkAndSlug
    query_pk_and_slug = False

Looking like:

infolinks script]

References

Uruguay
Marcelo Canina
I'm Marcelo Canina, a developer from Uruguay. I build websites and web-based applications from the ground up and share what I learn here.
comments powered by Disqus


Guide to handle slugs for Django models focusing in just using Django's SlugField and avoiding third party apps/packages. Two approaches to solve it.

Clutter-free software concepts.
Translations English Español

Except as otherwise noted, the content of this page is licensed under CC BY-NC-ND 4.0 . Terms and Policy.

Powered by SimpleIT Hugo Theme

·