Populating django field with pre_save()?

68,157

Solution 1

Most likely you are referring to django's pre_save signal. You could setup something like this:

from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.template.defaultfilters import slugify

@receiver(pre_save)
def my_callback(sender, instance, *args, **kwargs):
    instance.slug = slugify(instance.title)

If you dont include the sender argument in the decorator, like @receiver(pre_save, sender=MyModel), the callback will be called for all models.

You can put the code in any file that is parsed during the execution of your app, models.py is a good place for that.

Solution 2

@receiver(pre_save, sender=TodoList)
def my_callback(sender, instance, *args, **kwargs):
    instance.slug = slugify(instance.title)

Solution 3

you can use django signals.pre_save:

from django.db.models.signals import post_save, post_delete, pre_save

class TodoList(models.Model):
    @staticmethod
    def pre_save(sender, instance, **kwargs):
        #do anything you want

pre_save.connect(TodoList.pre_save, TodoList, dispatch_uid="sightera.yourpackage.models.TodoList") 

Solution 4

The pre_save() signal hook is indeed a great place to handle slugification for a large number of models. The trick is to know what models need slugs generated, what field should be the basis for the slug value.

I use a class decorator for this, one that lets me mark models for auto-slug-generation, and what field to base it on:

from django.db import models
from django.dispatch import receiver
from django.utils.text import slugify

def autoslug(fieldname):
    def decorator(model):
        # some sanity checks first
        assert hasattr(model, fieldname), f"Model has no field {fieldname!r}"
        assert hasattr(model, "slug"), "Model is missing a slug field"

        @receiver(models.signals.pre_save, sender=model, weak=False)
        def generate_slug(sender, instance, *args, raw=False, **kwargs):
            if not raw and not instance.slug:
                source = getattr(instance, fieldname)
                slug = slugify(source)
                if slug:  # not all strings result in a slug value
                    instance.slug = slug
        return model
    return decorator

This registers a signal handler for specific models only, and lets you vary the source field with each model decorated:

@autoslug("name")
class NamedModel(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField()

@autoslug("title")
class TitledModel(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField()

Note that no attempt is made to generate a unique slug value. That would require checking for integrity exceptions in a transaction or using a randomised value in the slug from a large enough pool as to make collisions unlikely. Integrity exception checking can only be done in the save() method, not in signal hooks.

Share:
68,157
Derek
Author by

Derek

Updated on July 05, 2022

Comments

  • Derek
    Derek almost 2 years
    class TodoList(models.Model):
        title = models.CharField(maxlength=100)
        slug = models.SlugField(maxlength=100)
        def save(self):
            self.slug = title
            super(TodoList, self).save()
    

    I'm assuming the above is how to create and store a slug when a title is inserted into the table TodoList, if not, please correct me!

    Anyhow, I've been looking into pre_save() as another way to do this, but can't figure out how it works. How do you do it with pre_save()?

    is it like

    def pre_save(self):
         self.slug = title
    

    I'm guessing no. What is the code to do this?

    Thanks!

  • user1066101
    user1066101 almost 13 years
    @Derek: Just override save(). It's much, much simpler and more predictable.
  • Bernhard Vallant
    Bernhard Vallant almost 13 years
    Better? It does basically the same... If you want to change an existing app's functionality going with the signal is for sure the preferred way...
  • xyres
    xyres about 10 years
    Awesome. This answer should be included in Django docs. There's really no example in docs about using signals.
  • xyres
    xyres almost 10 years
    @Firula Right, you are. I should have rather said no solid example. Thanks BTW.
  • Simon Steinberger
    Simon Steinberger almost 9 years
    Also, pre_save is called even for list updates, whereas the save method is only used when actually calling save on an instance. Thus, pre_save is the only choice if you do something like MyModel.objects.all().update(field_x=x).
  • msanders
    msanders about 7 years
    @simon-steinberger pre_save is not called when you use the update method on QuerySets - the Django docs say "Finally, realize that update() does an update at the SQL level and, thus, does not call any save() methods on your models, nor does it emit the pre_save or post_save signals (which are a consequence of calling Model.save())."
  • Engin Yapici
    Engin Yapici over 4 years
    @msanders do you know a way to make this work with QuerySet update method?
  • MikeyE
    MikeyE over 4 years
    For whatever reason the accepted solution did not work for me. But, this clean and elegant solution did.
  • Ed Menendez
    Ed Menendez over 3 years
    If you're going to modify the model you might as well modify the save() method. Signals should only be used if you need to decouple the code from the model.
  • Gnietschow
    Gnietschow about 3 years
    I needed to add weak=False to the @receiver decorator to get this working. Local functions can be garbage collected otherwise as stated in the docs (docs.djangoproject.com/en/3.0/ref/signals/…), which prevented my functions from being called.
  • Martijn Pieters
    Martijn Pieters about 3 years
    @Gnietschow: ah, good point! I happen to have a more involved implementation that I've simplified down for this answer, and.. that lost a reference. So I didn't run into that issue myself. I'll update the answer.