30

I am working on a notification app in Django 1.6 and I want to pass additional arguments to Django signals such as post_save. I tried to use partial from functools but no luck.

from functools import partial
post_save.connect(
    receiver=partial(notify,
        fragment_name="categories_index"),
            sender=nt.get_model(),
            dispatch_uid=nt.sender
    )

notify function has a keyword argument fragment_name which I want to pass as default in my signals.

Any suggestions?

Paolo
  • 20,112
  • 21
  • 72
  • 113
Mo J. Mughrabi
  • 6,747
  • 16
  • 85
  • 143

5 Answers5

34

You can define additional arguments in custom save method of model like this:

class MyModel(models.Model):
    ....

    def save(self, *args, **kwargs):
        super(MyModel, self).save(*args, **kwargs)
        self.my_extra_param = 'hello world'

And access this additional argument through instance in post_save signal receiver:

@receiver(post_save, sender=MyModel)
def process_my_param(sender, instance, *args, **kwargs):
    my_extra_param = instance.my_extra_param
Eugene Soldatov
  • 9,755
  • 2
  • 35
  • 43
  • In this case, what is the purpose of using getattr() to access the additional arg in the post-save receiver (instead of just accessing it directly)? – Troy Sep 04 '14 at 17:38
  • You are right, it's unnecessary to call getattr, parameter is accessible directly. I have edited answer, thanks. – Eugene Soldatov Sep 05 '14 at 10:29
  • 3
    This answer is erroneous. If it were to work, then `self.my_extra_param = 'hello world'` would need to be executed before the call to the super save method. – Ren Apr 08 '20 at 17:54
21

Your attempt with partial isn't working because by default these receivers are connected using a weak reference.

According to the Django docs:

Django stores signal handlers as weak references by default, so if your handler is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect().

from functools import partial
post_save.connect(
    receiver=partial(notify,
        fragment_name="categories_index"),
            sender=nt.get_model(),
            dispatch_uid=nt.sender,
            weak=False
    )

Include weak=False and this partial won't be garbage collected.

My original answer is below and took an approach that wasn't using partial.

You could decorate your post save function prior to connecting it with the post_save receiver.

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

def extra_args(fragment_name, *args, **kwargs):
    def inner1(f, *args, **kwargs):
        def inner2(sender, instance, **kwargs):
            f(sender, instance, fragment_name=fragment_name, **kwargs)
        return inner2
    return inner1

@receiver(post_save, sender=ExampleModel)
@extra_args(fragment_name="categories_index")
def my_post_save(sender, instance, fragment_name, **kwargs):
    print "fragment_name : ", fragment_name
    #rest of post save...

The extra inner in extra_args is for decorators that take parameters.

If you want to do this programmatically this works the same way but note that you need to include weak=False to have the wrapped function not be garbage collected.

receiver(post_save, sender=aSenderClass, weak=False)(extra_args(fragment_name="meep")(my_post_save))

Or without wrapping, but calling post_save.connect like your original attempt with partial

post_save.connect(extra_args(fragment_name="meepConnect")(my_post_save), sender=Author, weak=False)
Community
  • 1
  • 1
Daniel Rucci
  • 2,822
  • 2
  • 32
  • 42
  • 1
    Hi Dan, your approach looks solid and logical. I tried to implement it though but django signal is not picking it up, any ideas what I did wrong? here is my startup.py script which executes once on django load https://gist.github.com/mo-mughrabi/11517411 – Mo J. Mughrabi May 04 '14 at 14:20
  • 1
    Hey, took a look at that code and you are correct. I had experienced some weird behavior and thought it was just my shell. It seems that if you add weak=False then it works – Daniel Rucci May 04 '14 at 17:45
  • What if the model in question is Django's User model (I don't control it's save function). I also cannot use https://stackoverflow.com/questions/11179380/pass-additional-parameters-to-post-save-signal technique because my `post_save` handler is triggered immediately. I'm tearing my hair out. – Csaba Toth Dec 08 '17 at 08:23
  • This looks like a great solution! I am wondering tough, what does happen to garbage collection if we use `weak=False`? – Novarac23 Feb 21 '18 at 18:39
10

I tried Eugene Soldatov's answer, but it made me realize it could be much simpler:

You could have something like:

obj = MyModel.objects.first()
obj.my_extra_param = "hello world"
obj.save() # this will trigger the signal call

and then have the receiver like in Eugene's answer, and it would work all the same.

@receiver(post_save, sender=MyModel)
def process_my_param(sender, instance, *args, **kwargs):
    my_extra_param = instance.my_extra_param

No need to create a custom save method prone to bugs.

This is how it currently works in Django 3.0. I haven't tried prior versions.

Why this happens? Good ol' documentation has the answer for you: https://docs.djangoproject.com/en/3.2/ref/models/instances/#what-happens-when-you-save

Ren
  • 4,594
  • 9
  • 33
  • 61
  • working of post_save I understood. But how come there is no attrbiute error when obj.my_extra_param = "hello world" is done ? – Sandeep Balagopal Aug 17 '21 at 08:57
  • 2
    Because it is written only on the object, not on the database level. As any python object, it is able to hold newly defined fields and be passed around with this data. Whenever you do `obj.refresh_from_db()` the value will be gone. – Ren Aug 19 '21 at 20:43
  • 1
    Thanks, @Ren.This should be an accepted answer easy and working – Maninder Singh Sep 22 '22 at 11:27
  • simple and great! – OlegТ Mar 18 '23 at 21:04
4

If the predefined signals are not suitable, you can always define your own.

import django.dispatch

custom_post_save = django.dispatch.Signal(providing_args=[
    "sender", "instance", "created", "raw", "using", "update_fields", "fragment_name"
])

Then in your Model you just have to override save() method:

from django.db import router

class YourModel(Model):

    # Your fields and methods

    def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
         custom_signal_kwargs = {
             "sender": self.__class__,
             "instance": self,
             "created": self.pk is None,
             "raw": False, # As docs say, it's True only for fixture loading
             "using": using or router.db_for_write(self.__class__, instance=self),
             "update_fields": update_fields,
             "fragment_name": "categories_index" # The thing you want
         }
         super(YourModel, self).save(force_insert=False, force_update=False, using=None,
             update_fields=None)
         custom_post_save.send(**custom_signal_kwargs) # Send custom signal

Now you just have to connect this custom signal to your notify(...) receiver and it will get fragment_name in kwargs.

ElmoVanKielmo
  • 10,907
  • 2
  • 32
  • 46
1

The code in Django responsible for Signals is defined here https://github.com/django/django/blob/master/django/dispatch/dispatcher.py. See how it inspects the receiver? I suspect your problems lie there. Maybe what you want is a wrapper function that honors the arguments a signal needs to have but also sets the value of fragment_name.

def fragment_receiver(sender, **kwargs)
    return notify(sender, fragment_name="categories_index", **kwargs)
Sean Perry
  • 3,776
  • 1
  • 19
  • 31
  • but how will the wrapper to send different values for fragment_name? when registering with post_save, I need to pass it that extra argument so i can access it inside my signal handler.. can you elaborate more please on your approach – Mo J. Mughrabi Apr 18 '14 at 02:50