I am making a documentation site to self-host for myself; learning Django at the same time. I am at the point where I am starting to populate the website.
I am wondering if there is a possibility to have two posts that have the same slug, but they are part of a different category.
For example, I have two posts that are named "virus". One is under the category of "computer science" and the other is under the category of "heathcare".
models.py
from django.contrib.auth.models import User
from django.db import models
from tinymce.models import HTMLField
from .fields import LowerCaseTagCharField
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
slug = models.SlugField(unique=False)
class Meta:
verbose_name = 'Category'
verbose_name_plural = 'Categories'
def __str__(self):
return self.name
class Reference(models.Model):
name = models.CharField(max_length=100)
description = HTMLField()
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='reference_category')
slug = models.SlugField(unique=False)
author = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
verbose_name = 'Reference'
verbose_name_plural = 'References'
def __str__(self):
return self.name
The website would link those pages as (this is just an example):
.../healthcare/virus/
.../computer-science/virus/
Is there a way to use the same slug depending on the value of another field (e.g. category field) or some other technique?
TIA
The fix I managed to implement:
forms.py
from django import forms
from django.core.exceptions import ValidationError
from reference.models import Reference, Category
class CreateReferenceForm(forms.ModelForm):
class Meta:
model = Reference
fields = '__all__'
def clean(self):
cleaned_data = super().clean()
category = cleaned_data.get('category')
slug = cleaned_data.get('slug')
refs = Reference.objects.filter(category=category)
for i in refs:
if i.slug == slug:
raise ValidationError("This slug already exists for a reference within the selected category.")
You can have duplicate slugs as long as they are are unique within their respective categories. The best way to handle this in Django is by enforcing uniqueness at the database level using the unique_together
constraint.
You can eventually override the save()
method to automatically generate slugs if duplicates exist:
from django.db import models
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=255, unique=True)
slug = models.SlugField(unique=True)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=255)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
slug = models.SlugField(max_length=255)
# Other fields ...
class Meta:
# Your Meta here ...
constraints = [
models.UniqueConstraint(fields=['slug', 'category'], name='unique_slug_per_category')
]
def save(self, *args, **kwargs):
# Generate a slug if it's not set
if not self.slug:
base_slug = slugify(self.title)
self.slug = base_slug
count = 1
while Post.objects.filter(slug=self.slug, category=self.category).exists():
self.slug = f"{base_slug}-{count}"
count += 1
super().save(*args, **kwargs)
def __str__(self):
return f"{self.category.name} - {self.title}"
In this way each Post is linked to a Category, while the Unique Constraint ensures that the same slug cannot be repeated within the same category. Finally, the save()
method generates a slug from the title if it’s not provided (it also checks if a duplicate exists, appending a number (slug-1, slug-2, etc.) to make it unique).
URLs can be defined like:
urlpatterns = [
path('<slug:category_slug>/<slug:slug>/', YourView.as_view(), name='post-detail'),
]
Hope it helped