How to create random slug in Django

Viewed 409

I have a Django app that will create a contract between two people. The user creates the contract and, upon creation, a random url-safe string is generated using secrets.

However, when I create more than one instance of a Contract, the slugs are the same. The only way to get a different randomly generated string is to restart my server. What am I doing wrong?

Here's the part that matters in my models.py

    slug = models.SlugField(
        default=secrets.token_urlsafe(64),
        editable=False,
        blank=False
    )

Here are the last three Contracts I created. Changing the values of the other fields doesn't change the slug.

    {
        "model_email": "POOP@pooop.com",
        "shoot_location": "Atlanta, GA",
        "shoot_date": "2020-05-24",
        "model_name": "",
        "created_at": "2020-05-24T19:24:20.220845Z",
        "sent_completed_to_model": null,
        "sent_completed_to_photographer": null,
        "photographer": 1,
        "signed": false,
        "slug": "Dqyk5-kDzE219_-abRKABETRuOCGOBxzvFVrXZaUGEe8ZLva6i6tdwUYMksSvKJ_BEABV_Vt1H_ttZ6p-yzRyg"
    },
    {
        "model_email": "POOP@pooop.com",
        "shoot_location": "Atlanta, GA",
        "shoot_date": "2020-05-24",
        "model_name": "",
        "created_at": "2020-05-24T19:24:21.216218Z",
        "sent_completed_to_model": null,
        "sent_completed_to_photographer": null,
        "photographer": 1,
        "signed": false,
        "slug": "Dqyk5-kDzE219_-abRKABETRuOCGOBxzvFVrXZaUGEe8ZLva6i6tdwUYMksSvKJ_BEABV_Vt1H_ttZ6p-yzRyg"
    },
    {
        "model_email": "POOP@pooop.com",
        "shoot_location": "Atlanta, GA",
        "shoot_date": "2020-05-24",
        "model_name": "",
        "created_at": "2020-05-24T19:24:22.120845Z",
        "sent_completed_to_model": null,
        "sent_completed_to_photographer": null,
        "photographer": 1,
        "signed": false,
        "slug": "Dqyk5-kDzE219_-abRKABETRuOCGOBxzvFVrXZaUGEe8ZLva6i6tdwUYMksSvKJ_BEABV_Vt1H_ttZ6p-yzRyg"
    }
3 Answers

This happen because your default value is not function that will be called on each save, it's rather a constant value generated when your app start

An other option would be moving the logic to save method of your model:

    slug = models.SlugField(
        default='',
        editable=False,
        blank=False
    )

    ...

    def save(self, *args, **kwargs):
        if not self.default:
            self.default = secrets.token_urlsafe(64)

        super().save(*args, **kwargs)

Change your code to:

def get_token_slug():
    return secrets.token_urlsafe(64)

...

slug = models.SlugField(
        default=get_token_slug,
        editable=False,
        blank=False,
        unique=True,             # also add this
    )

The problem was that the function call that you give as a default value is computed once and the result is used as a default value in all new instances of this model.

Instead, you should give a callable as a default value, which will then be called when a new instance of the model is created. So just wrap the token_urlsafe call in a function, and give it as an argument (the function, not a call, so without parentheses () )

In terms of creating a unique slug field when saving an instance of your model, you will need to alter your save method so that it can create a slug which has the next available number appended to the end of your slug. It might look something like the below...

Replace MyModel and field_you_are_using_to_slugify as applicable.

def save(self):
    if not self.slug.strip():
        self.slug = slugify(self.field_you_are_using_to_slugify)

    _slug = self.slug
    _count = 1

    while True:
        try:
            MyModel.objects.all().exclude(pk=self.pk).get(slug=_slug)
        except MultipleObjectsReturned:
            pass
        except ObjectDoesNotExist:
            break
        _slug = f"{self.slug}{_count}"
        _count += 1

    self.slug = _slug

    super(MyModel, self).save()

It should be noted however that if you're after a URL which cannot be interpreted and uses random string generation, then Django provides a UUID field for that purpose. Here is a link to the docs

Related