5
class Product(models.Model):
    title = models.CharField(max_length=75)

class Deal(models.Model):
    product = models.ForeignKey(Product)
    slug = models.SlugField(max_length=255, unique=True)

Having a similar basic setup as above, I want to generate unique slugs for each Deal instance using product title of it's deal and id of the deal itself. IE: "apple-iphone-4s-161" where 161 is the id of the deal and the text before is the title of the product.

For this, how can I overwrite the save() method of the Deal model to apply it?

Hellnar
  • 62,315
  • 79
  • 204
  • 279

4 Answers4

8

Of course you can simply overwrite save() method on model (or make receiver for post_save signal). It will be something like:

from django.template.defaultfilters import slugify

class Deal(models.Model):
product = models.ForeignKey(Product)
slug = models.SlugField(max_length=255, unique=True)

    def save(self, *args, **kwargs):
        super(Deal, self).save(*args, **kwargs)
        if not self.slug:
            self.slug = slugify(self.product.title) + "-" + str(self.id)
            self.save()

But what is ugly in this solution is that it will hit database twice (it is saved two times). It is because when creating new Deal object it will not have id until you save it for the first time (and you cannot do much about it).

jasisz
  • 1,288
  • 8
  • 9
  • 1
    It's not ugly, it's the only way to do it. – devmiles.com Aug 16 '12 at 19:07
  • 1
    It is ugly - because idea of such slug is not nice itself. I know it is easier to do it like that and forgot about, but in fact such slug is nothing more than denormalization of database. I think it would be better to refer to object by its' id and just faking the use of slug in views and urls. – jasisz Aug 16 '12 at 22:57
  • this is not quite correct, you are doing save 2 times nonsense, the super call goes in the bottom – Jota Jun 04 '21 at 10:21
  • @Jota i think you need to call `Super` so you get an id. Also, beaware the default length of the SlugField is 50, so you may want to truncate your title in this case `self.slug = slugify(self.product.title[45]) + "-" + str(self.id)` – its30 Apr 03 '23 at 23:32
3

i've bumped at this problem and tested the jasisz solution, and got the max recursion depth exceeded error, so i've fiddle it little and this is how looks for me:

def save(self, *args, **kwargs):
    if not self.id:
        self.slug = slugify(self.title)
    super(Node, self).save(*args, **kwargs)

You could edit this to suit your needs, it tests if this records exists, if not then it creates the slug field otherwise is update and no need for modifieng the slug field.

hope it helps.

  • Thanks! I forgot about it completely when writing "from head". – jasisz Aug 16 '12 at 22:53
  • But that wont save the id into the slug. It will only slugify the title, and for OP example it seems that many titles can be the same. That's why he wants to added a unique id so that slug wont throw a duplicate exception. – Algorithmatic Nov 05 '14 at 04:45
3

You should not have your id in that slug field at all. The two main reasons for this are:

  1. It creates the problem you are having
  2. It's redundant data in the database – the id is already stored

But there is a simple solution to your problem: The only reason why the slug is stored in the database is so you can find a row by slug. But you don't need that – you have the ID. So you should do this:

class DealManager(models.Manager):
    def get_by_slug(slug):
       return self.get(id=slug.rsplit('-', 1)[1])


class Deal(models.Model):
    product = models.ForeignKey(Product)

    objects = DealManager()

    @property
    def slug(self):
        return slugify(f'{self.name}-f{self.id}')

In your view or wherever you need to retrieve an item given a slug, you can just do Deal.objects.get_by_slug(slug).

Chronial
  • 66,706
  • 14
  • 93
  • 99
2

I know this may not exactly work for your situation, but I remember bumping into a similar case. I think I had a created_at field in my model that has auto_now=True. Or something simillar

My slug would look like like this

self.slug = '%s-%s' %(
        slugify(self.title),
        self.created_at
)

or you can have

self.slug = '%s-%s' %(
        slugify(self.title),
        datetime.datetime.now()
)

just make sure that the slug max_length is long enough to include the full created_at time, as well as the title, so that you don't end up with non-unique or over max length exceptions.

Algorithmatic
  • 1,824
  • 2
  • 24
  • 41