Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django signals - kwargs['update_fields'] is always None on model update via django admin

I have a signal inside my django app where I would like to check if a certain field in my model has been updated, so I can then proceed and do something.

My model looks like this...

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.PositiveIntegerField()
    tax_rate = models.PositiveIntegerField()
    display_price = models.PositiveInteger()
    inputed_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL)
    updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL)

My signal looks like this...

@receiver(post_save, sender=Product)
def update_model(sender, **kwargs):
    instance = kwargs['instance']
    if 'tax_rate' in kwargs['update_fields']:
        # do something

This returns the error None is not an iterable. I have read the django signal documentation regarding the update_fields and it says The set of fields to update as passed to Model.save(), or None if update_fields wasn’t passed to save().

I should mention that I am working inside django admin here so what I hoped would happen is, I could create an instance of my Product model in django admin and then later if the value of tax_rate or price were updated, I could check for those and update the list_price accordingly. However, kwargs['update_fields'] always returns None. What am I getting wrong? Or is there some other way I could achieve that result inside django admin?

Updated section

Now, say I introduce a field called inputed_by in my product model, that points to the user model and I want that field populated when the model is first saved. Then another field updated_by that stores the user who last updated the model. At the same time I wish to check whether either or both the tax_rate or price has been updated.

Inside my model admin I have the following method...

def save_model(self, request, obj, form, change):
    update_fields = []
    if not obj.pk:
        obj.inputed_by = request.user
    elif change:
        obj.updated_by = request.user

        if form.initial['tax_rate'] != form.cleaned_data['tax_rate']:
            update_fields.append('tax_rate')
        if form.initial['price'] != form.cleaned_data['price']:
            update_fields.append('price')

    obj.save(update_fields=update_fields)
    super().save_model(request, obj, form, change)

My signal now looks like this...

@receiver(post_save, sender=Product, dispatch_uid="update_display_price")
def update_display_price(sender, **kwargs):
    created = kwargs['created']
    instance = kwargs['instance']
    updated = kwargs['update_fields']
    checklist = ['tax_rate', 'price']

    # Prints out the frozenset containing the updated fields and then below that `The update_fields is None`

    print(f'The update_fields is {updated}')

    if created:
        instance.display_price = instance.price+instance.tax_rate
        instance.save()
    elif set(checklist).issubset(updated):
        instance.display_price = instance.price+instance.tax_rate
        instance.save() 

I get the error 'NoneType' object is not iterable The error seems to come from the line set(checklist).issubset(updated). I've tried running that line specifically inside the python shell and it yields the desired results. What's wrong this time?

like image 481
Nelson King Avatar asked Feb 07 '19 16:02

Nelson King


People also ask

How do I register a signal in Django?

To receive a signal, register a receiver function using the Signal. connect() method. The receiver function is called when the signal is sent. All of the signal's receiver functions are called one at a time, in the order they were registered.

Where is Pre_save signal in Django?

pre_save. This is sent at the beginning of a model's save() method. Arguments sent with this signal: sender.

What is the use of the Post_delete signal in Django?

There are 3 types of signal. pre_save/post_save: This signal works before/after the method save(). pre_delete/post_delete: This signal works before after delete a model's instance (method delete()) this signal is thrown.

Are Django signals asynchronous?

To answer directly: No. It's sync.


2 Answers

The set of fields should be passed to Model.save() to make them available in update_fields.

Like this

model.save(update_fields=['tax_rate'])

If you are creating something from django admin and getting always None it means that update_fields has not been passed to model's save method. And because of that it will always be None.

If you check ModelAdmin class and save_model method you'll see that call happens without update_fields keyword argument.

enter image description here

It will work if you write your own save_model.

The code below will solve your problem:

class ProductAdmin(admin.ModelAdmin):
    ...
    def save_model(self, request, obj, form, change):
        update_fields = []

        # True if something changed in model
        # Note that change is False at the very first time
        if change: 
            if form.initial['tax_rate'] != form.cleaned_data['tax_rate']:
                update_fields.append('tax_rate')

        obj.save(update_fields=update_fields)

Now you'll be able to test memberships in update_model.

like image 126
Davit Tovmasyan Avatar answered Sep 20 '22 09:09

Davit Tovmasyan


To add to Davit Tovmasyan's post. I made a more universal version that covers any field change using a for loop:

class ProductAdmin(admin.ModelAdmin): 
    ...
    def save_model(self, request, obj, form, change):
        update_fields = []
        for key, value in form.cleaned_data.items():
            # True if something changed in model
            if value != form.initial[key]:
                update_fields.append(key)

        obj.save(update_fields=update_fields)

EDIT: WARNING This isnt actually a full solution. Doesnt seem to work for object creation, only changes. I will try to figure out the full solution soon.

like image 29
Benargee Avatar answered Sep 20 '22 09:09

Benargee