from django.db import models
from django.utils.text import slugify
class Article(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=220, unique=True, blank=True)
body = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
def build_unique_slug(self):
base = slugify(self.title)[:200] or "article"
candidate = base
suffix = 2
qs = Article.objects.all()
if self.pk:
qs = qs.exclude(pk=self.pk)
while qs.filter(slug=candidate).exists():
candidate = "{0}-{1}".format(base, suffix)
suffix += 1
return candidate
from django.db.models.signals import pre_save
from django.dispatch import receiver
from .models import Article
@receiver(pre_save, sender=Article)
def ensure_article_slug(sender, instance, **kwargs):
title_changed = True
if instance.pk:
old = sender.objects.filter(pk=instance.pk).only("title").first()
if old is not None:
title_changed = old.title != instance.title
if not instance.slug or title_changed:
instance.slug = instance.build_unique_slug()
from django.apps import AppConfig
class ArticlesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "articles"
def ready(self):
from . import signals # noqa: F401 registers pre_save receiver
This snippet shows a common Django task: generating a URL-friendly slug from a title and guaranteeing it is unique, without repeating the logic every time an object is saved. The work is split so the model owns the slug-building rules while a pre_save signal enforces them automatically on every write.
In articles/models.py, Article exposes a build_unique_slug method rather than baking the logic into save. It uses slugify to normalize the title, then loops, appending a numeric suffix (-2, -3, ...) until it finds a value not already taken. The uniqueness check excludes the current row via exclude(pk=self.pk) so an update that keeps the same slug does not collide with itself. The method is deliberately side-effect-free — it returns a string instead of assigning it — which makes it easy to unit test and reuse. Keeping unique=True on the field means the database is the final arbiter, so even a race that slips past the Python check still fails loudly instead of silently duplicating.
The term "debounce" here means the slug is only recomputed when it actually needs to be: an empty slug, or a title change on a persisted object. In articles/signals.py, the ensure_article_slug receiver is wired to pre_save. It computes title_changed by loading the old row and comparing, but only when the instance already has a pk. New objects, or objects whose slug is blank, always get a fresh slug; existing objects only regenerate when their title moved. This avoids churning the slug — and breaking existing URLs — on unrelated field updates.
Computing the slug in pre_save rather than save keeps the behavior consistent no matter how the object is written, including bulk-ish flows and admin saves that call save directly. articles/apps.py connects the receiver inside ready(), the canonical place to register signals so they load exactly once. The main trade-off is implicitness: signals hide behavior from the model's own save, so a developer reading Article alone might not see where slugs come from — hence the explicit method as documentation. For heavy-write systems the read-back in the signal adds a query per update, which a Meta.constraints unique index and retry-on-IntegrityError can complement.
Related snips
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Semantic HTML Example</title>
Semantic HTML5 elements and accessibility best practices
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2)
margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)
Django model signals vs overriding save
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
# Vulnerable: user input is concatenated directly into SQL.
email = params[:email]
password = params[:password]
sql = "SELECT * FROM users WHERE email = '#{email}' AND password_hash = '#{password}'"
user = ActiveRecord::Base.connection.execute(sql).first
SQL injection prevention with unsafe and safe query patterns
import graphene
from graphene_django import DjangoObjectType
from blog.models import Post, Comment
class PostType(DjangoObjectType):
Django GraphQL with Graphene
class AddSettingsToAccounts < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_column :accounts, :settings, :jsonb, null: false, default: {}
Postgres JSONB Partial Index for Feature Flags
Share this code
Here's the card — post it anywhere.