Generating slugs automatically in Django without packages - Two easy and solid approaches
Overview
One important task when developing a Django website is to have pretty
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:
pk and slugs: object/pk-slug like
/article/12345-my-example-title
where we add the object’s slug after the primary key.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:
- Truncate the slug to respect
max_length
- Control the slug uniqueness by adding a suffix if another slug with the same string exists1
- 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:
]
References
- https://www.owasp.org/index.php/OWASP_Testing_Guide_v4_Table_of_Contents
- Keep a simple URL structure https://support.google.com/webmasters/answer/76329?hl=en
- Slugify should take an optional max length https://code.djangoproject.com/ticket/15307
Slug Uniqueness Code based in https://keyerror.com/blog/automatically-generating-unique-slugs-in-django . ↩︎
- August 1, 2023
- How to create a reusable Django app and distribute it with PIP or publish to pypi.orgJune 29, 2021
- How To Serve Multiple Django Applications with uWSGI and Nginx in Ubuntu 20.04October 26, 2020
- How to add favicon to Django in 4 stepsSeptember 3, 2020
- Categories in Django with BreadcrumbsAugust 30, 2020
- How To Migrate From SQLite To PostgreSQL In Django In 3 stepsAugust 28, 2020
- Practical guide to internationalize a Django app in 5 steps.August 24, 2020
- Disable new users singup when using Django's allauth packageSeptember 3, 2019
- How to add ads.txt to Django as requested by Google AdsenseAugust 30, 2019
- Have multiple submit buttons for the same Django formJuly 2, 2019
- Better Testing with Page Object Design in DjangoMay 1, 2019
- Generating slugs automatically in Django without packages - Two easy and solid approaches
- How to set up Django tests to use a free PostgreSQL database in HerokuFebruary 13, 2019
- Dynamically adding forms to a Django FormSet with an add button using jQueryFebruary 6, 2019
- Use of Django's static templatetag in css file to set a background imageFebruary 1, 2019
- Activate Django's manage.py commands completion in Bash in 2 stepsJanuary 29, 2019
- Sending Emails with Django using SendGrid in 3 easy stepsJanuary 9, 2019
- Adding Users to Your Django Project With A Custom User ModelSeptember 21, 2018
- Setting Up A Factory For One To Many Relationships In FactoryboyApril 17, 2018
- Generate UML class diagrams from django modelsMarch 24, 2018
- Set Up Ubuntu To Serve A Django Website Step By StepJuly 3, 2017
- Django Project Directory StructureJuly 16, 2016
- How to Have Different Django Settings for Development and Production, and environment isolationJune 10, 2016
- Django OverviewJune 2, 2016
Django Forms
- Adding a Cancel button in Django class-based views, editing views and formsJuly 15, 2019
- Using Django Model Primary Key in Custom Forms THE RIGHT WAYJuly 13, 2019
- Django formset handling with class based views, custom errors and validationJuly 4, 2019
- How To Use Bootstrap 4 In Django FormsMay 25, 2018
- Understanding Django FormsApril 30, 2018
- How To Create A Form In DjangoJuly 29, 2016
Articles
Subcategories
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
·