34

I am trying to create a unique slug in Django so that I can access a post via a url like this: http://www.example.com/buy-a-new-bike_Boston-MA-02111_2

The relevant models:

class ZipCode(models.Model):
    zipcode = models.CharField(max_length=5)
    city = models.CharField(max_length=64)
    statecode = models.CharField(max_length=32)

class Need(models.Model):
    title = models.CharField(max_length=50)
    us_zip = models.CharField(max_length=5)
    slug = ?????

    def get_city():
        zip = ZipCode.objects.get(zipcode=self.us_zip)
        city = "%s, %s %s" % (zip.city, zip.statecode, zip.zipcode)
        return city

A sample ZipCode record:

  • zipcode = "02111"
  • city = "Boston"
  • statecode = "MA"

A sample Need record:

  • title = "buy a new bike"
  • us_zip = "02111"
  • slug = "buy-a-new-bike_Boston-MA-02111_2" (desired)

Any tips as to how to create this unique slug? Its composition is:

  • Need.title + "_" + Need.get_city() + "_" + an optional incrementing integer to make it unique. All spaces should be replaced with "-".

NOTE: My desired slug above assumes that the slug "buy-a-new-bike_Boston-MA-02111" already exists, which is what it has the "_2" appended to it to make it unique.

I've tried django-extensions, but it seems that it can only take a field or tuple of fields to construct the unique slug. I need to pass in the get_city() function as well as the "_" connector between the title and city. Anyone solved this and willing to share?

Thank you!

UPDATE

I'm already using django-extensions for its UUIDField, so it would be nice if it could also be usable for its AutoSlugField!

Cœur
  • 37,241
  • 25
  • 195
  • 267
mitchf
  • 3,697
  • 4
  • 26
  • 29

13 Answers13

47

I use this snippet for generating unique slug and my typical save method look like below

slug will be Django SlugField with blank=True but enforce slug in save method.

typical save method for Need model might look below

def save(self, **kwargs):
    slug_str = "%s %s" % (self.title, self.us_zip) 
    unique_slugify(self, slug_str) 
    super(Need, self).save(**kwargs)

and this will generate slug like buy-a-new-bike_Boston-MA-02111 , buy-a-new-bike_Boston-MA-02111-1 and so on. Output might be little different but you can always go through snippet and customize to your needs.

Community
  • 1
  • 1
Srikanth Chundi
  • 897
  • 9
  • 8
  • Thank you Srikanth! I just implemented this and it works great! – mitchf Sep 29 '10 at 02:41
  • 1
    Just curious, why are you overriding `save` instead of using signals? – art-solopov May 18 '16 at 22:06
  • 5
    I would avoid using signals in Django. I have found that they are just so hard to debug because you've typically moved your signal code into a separate file. It's very easy to forget about it. The time to use signals is when you want to make a modification to an instance, and that instance is not always going to be the same model. – Bobort Jan 30 '20 at 19:34
8

My little code:

def save(self, *args, **kwargs):
    strtime = "".join(str(time()).split("."))
    string = "%s-%s" % (strtime[7:], self.title)
    self.slug = slugify(string)
    super(Need, self).save()
Nips
  • 13,162
  • 23
  • 65
  • 103
6

If you are thinking of using an app to do it for you, here is one.

https://github.com/un33k/django-uuslug

UUSlug = (``U``nique + ``U``code Slug)


Unicode Test Example
=====================
from uuslug import uuslug as slugify

s = "This is a test ---"
r = slugify(s)
self.assertEquals(r, "this-is-a-test")

s = 'C\'est déjà l\'été.'
r = slugify(s)
self.assertEquals(r, "c-est-deja-l-ete")

s = 'Nín hǎo. Wǒ shì zhōng guó rén'
r = slugify(s)
self.assertEquals(r, "nin-hao-wo-shi-zhong-guo-ren")

s = '影師嗎'
r = slugify(s)
self.assertEquals(r, "ying-shi-ma")


Uniqueness Test Example
=======================
Override your objects save method with something like this (models.py)

from django.db import models
from uuslug import uuslug as slugify

class CoolSlug(models.Model):
    name = models.CharField(max_length=100)
    slug = models.CharField(max_length=200)

    def __unicode__(self):
        return self.name

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name, instance=self)
        super(CoolSlug, self).save(*args, **kwargs)

Test:
=====

name = "john"
c = CoolSlug.objects.create(name=name)
c.save()
self.assertEquals(c.slug, name) # slug = "john"

c1 = CoolSlug.objects.create(name=name)
c1.save()
self.assertEquals(c1.slug, name+"-1") # slug = "john-1"
Val Neekman
  • 17,692
  • 14
  • 63
  • 66
  • @IvanVirabyan by "concurrency issue" do you mean if it is thread-safe?If so, then it is as safe as Django itself and no more. – Val Neekman Sep 13 '13 at 14:53
  • 2
    Thread safety is not an issue here. I mean concurrency between different instances of application, both of them may generate identical slug (for example 'john-1') at the same time, and when they try to save the model they would get integrity error. – Ivan Virabyan Sep 16 '13 at 07:32
  • I guess you cannot totally remove the collision possibility. If you have an enhancement patch, by all means, send a pull request. – Val Neekman Oct 02 '13 at 14:00
  • 1
    A word of caution: Although django-uuslug is licenced under BSD, django-uuslug (1.1.8) uses python-slugify (1.2.4) which uses Unidecode (0.4.20), which is licenced under GPL. – Niko Föhr May 20 '17 at 19:57
6

Here are a couple functions that I use. You pass in the model instance and the desired title into unique_slugify which will add the slug if it doesn't exist, otherwise it will continue trying to append a 4 digit random string until it finds a unique one.

import string
from django.utils.crypto import get_random_string

def unique_slugify(instance, slug):
    model = instance.__class__
    unique_slug = slug
    while model.objects.filter(slug=unique_slug).exists():
        unique_slug = slug + get_random_string(length=4)
    return unique_slug

I usually use it by overriding the model save method.

class YourModel(models.Model):
    slug = models.SlugField()
    title = models.CharField()

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = unique_slugify(self, slugify(self.title))
        super().save(*args, **kwargs)
bdoubleu
  • 5,568
  • 2
  • 20
  • 53
4

This is the simple and small code i am using for generating unique slug, you only need one field to create your unique slug field

from random import randint

def save(self, *args, **kwargs):
    if Post.objects.filter(title=self.title).exists():
        extra = str(randint(1, 10000))
        self.slug = slugify(self.title) + "-" + extra
    else:
        self.slug = slugify(self.title)
    super(Post, self).save(*args, **kwargs)

I hope you like this.

Deepanshu Mehta
  • 1,119
  • 12
  • 9
2

This is a simple implementation that generate the slug from the title, it doesn't depend on other snippets:

from django.template.defaultfilters import slugify

class Article(models.Model):
    ...
    def save(self, **kwargs):
        if not self.slug:
            slug = slugify(self.title)
            while True:
                try:
                    article = Article.objects.get(slug=slug)
                    if article == self:
                        self.slug = slug
                        break
                    else:
                        slug = slug + '-'
                except:
                    self.slug = slug
                    break

        super(Article, self).save()
ahmed
  • 5,430
  • 1
  • 20
  • 36
2

Django provides a SlugField model field to make this easier for you. Here's an example of it in a "blog" app's

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField(blank=True)

    slug = models.SlugField(unique=True)

    @models.permalink
    def get_absolute_url(self):
        return 'blog:post', (self.slug,)

Note that we've set unique=True for our slug field — in this project we will be looking up posts by their slug, so we need to ensure they are unique. Here's what our application's views.py might look like to do this:

from .models import Post

def post(request, slug):
    post = get_object_or_404(Post, slug=slug)

    return render(request, 'blog/post.html', {
        'post': post,
    })
Community
  • 1
  • 1
frostcs
  • 353
  • 5
  • 10
  • I don't think you understand how `Unique` works. If True, this field must be unique throughout the table. This is enforced at the database level and by model validation. If you try to save a model with a duplicate value in a unique field, a django.db.IntegrityError will be raised by the model’s save() method. https://docs.djangoproject.com/en/2.1/ref/models/fields/#unique It does not mean it will make the field unique for you. – dbinott Apr 02 '19 at 14:34
  • I'm using a slugfield, how can I get it unique, I read the other answers, but I don't understand how it works, or where to put the code. – AnonymousUser Jul 21 '21 at 05:22
2

Hi can you tried this function

class Training(models.Model):
    title = models.CharField(max_length=250)
    text = models.TextField()
    created_date = models.DateTimeField(
    auto_now_add=True, editable=False, )
    slug = models.SlugField(unique=True, editable=False, max_length=250)

    def __unicode__(self):
       return self.title

    def get_unique_slug(id,title,obj):
      slug = slugify(title.replace('ı', 'i'))
      unique_slug = slug
      counter = 1
      while obj.filter(slug=unique_slug).exists():
         if(obj.filter(slug=unique_slug).values('id')[0]['id']==id):
             break
         unique_slug = '{}-{}'.format(slug, counter)
         counter += 1
      return unique_slug.  

    def save(self, *args, **kwargs):
       self.slug =self.get_unique_slug(self.id,self.title,Training.objects)
       return super(Training, self).save(*args, **kwargs)
Ayse
  • 576
  • 4
  • 13
Cihan baş
  • 79
  • 5
2

from django.utils.text import slugify Helps a lot and has quite clear Concepts. Here one example on How to auto-generate slug by using from django.utils.text import slugify

utils.py

from django.utils.text import slugify
import random
import string

# Random string generator
def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))

# Unique Slug Generator 
def unique_slug_generator(instance, new_slug=None):
    """
    It assumes your instance has a model with a slug field and a title character (char) field.
    """
    if new_slug is not None:
        slug = new_slug  
    else:
        slug = slugify(instance.title)  

    Klass = instance.__class__

    qs_exists = Klass.objects.filter(slug=slug).exists()

    if qs_exists:
        new_slug = "{slug}-{randstr}".format(slug=slug, randstr=random_string_generator(size=4))
        return unique_slug_generator(instance, new_slug=new_slug)
    return slug

models.py

from django.db.models.signals import pre_save # Signals
# import the unique_slug_generator from .utils.py 
from .utils import unique_slug_generator

class Product(models.Model):
    title  = models.CharField(max_length=120)
    # set blank to True
    slug  = models.SlugField(blank=True, unique=True)

def product_pre_save_receiver(sender, instance, *args, **kwargs):
    if not instance.slug:
        instance.slug = unique_slug_generator(instance)


pre_save.connect(product_pre_save_receiver, sender=Product)

Django documentation explains Django.utils.text import slugify to generate slug automatically. You can read more detail here

After implementing the code, while creating product, you may leave the slug field blank, which will be further aquired with auto generated slug for the product which will be unique in this case.

Manish
  • 501
  • 4
  • 11
0
 def get_slug(self):
    slug = slugify(self.title.replace("ı", "i"))
    unique = slug
    number = 2

    while Model.objects.filter(slug=unique).exists():
        unique = "{}-{}".format(slug, number)
        number += 1
    return unique
Ayse
  • 576
  • 4
  • 13
0

Best solution for me:

def get_slug(self):
    slug = slugify(self.title)
    unique_slug = slug

    number = 1
    while Recipe.objects.filter(slug=unique_slug).exists():
        unique_slug = f'{slug}-{number}'
        number += 1

    return unique_slug

def save(self, *args, **kwargs):
    if not self.slug:
        self.slug = self.get_slug()
    return super().save(*args, **kwargs)

This code can generate slug like this:

  • string-slug
  • string-slug-1 (if previous alredy exists)
  • string-slug-2 (if previous alredy exists)
  • and so on...
-1
class Need(models.Model):
    title = models.CharField(max_length=50)
    us_zip = models.CharField(max_length=5)
    slug = models.SlugField(unique=True)

    def save(self, **kwargs):
        slug_str = "%s %s" % (self.title, self.us_zip) 
        super(Need, self).save()
Yeo
  • 11,416
  • 6
  • 63
  • 90
  • This is wrong, ``Need.slug`` remains unchanged after ``save()``. Moreover, the value suggested is not appropriate for use in a URL (which is what has been asked for). – alekosot Sep 20 '14 at 21:50
  • I think qingfeng forgot to add `**kwargs` in `save` method. `super(Need, self).save(**kwargs)` – Arun V Jose Nov 01 '16 at 02:58
-1

Try this, worked out for me,welcome in advance:

class Parcel(models.Model):
    title = models.CharField(max_length-255)
    slug = models.SlugField(unique=True, max_length=255)
    weight = models.IntegerField()
    description = models.CharField(max_length=255)
    destination = models.CharField(max_length=255)
    origin = models.CharField(max_length=255)

    def __str__(self):
        return self.description

    def save(self, *args, **kwargs):
        if not self.slug:
            t_slug = slugify(self.title)
            startpoint = 1
            unique_slug = t_slug
            while Parcel.objects.filter(slug=unique_slug).exists():
                unique_slug = '{} {}'.format(t_slug, origin)
                origin += 1
            self.slug = unique_slug
        super().save(*args, **kwargs)
Kelvin Onkundi
  • 139
  • 2
  • 2