djangodjango-modelsdjango-viewsdjango-templatesdjango-urls

Django same slug but using different URL path


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):

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.")

Solution

  • 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