Reputation: 805
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?
Upvotes: 12
Views: 18420
Reputation: 1330
Just for anyone who is coming in!
I believe this is the complete solution to this case, note that if you have any ManyToMany fields in your model then you should skip adding them to update fields
def save_model(self, request, obj, form, change):
"""
Given a model instance save it to the database.
"""
update_fields = set()
if change:
for key, value in form.cleaned_data.items():
# assuming that you have ManyToMany fields that are called groups and user_permissions
# we want to avoid adding them to update_fields
if key in ['user_permissions', 'groups']:
continue
if value != form.initial[key]:
update_fields.add(key)
obj.save(update_fields=update_fields)
Upvotes: 1
Reputation: 257
You can do this.
def save_model(self, request, obj, form, change):
if change:
obj.save(update_fields=form.changed_data)
else:
super().save_model(request, obj, form, change)
Upvotes: 2
Reputation: 3730
I wanted to add an alternative that relies on the pre_save signal to get the previous version of the instance you're evaluating (from this SO answer):
@receiver(pre_save, sender=Product)
def pre_update_model(sender, **kwargs):
# check if the updated fields exist and if you're not creating a new object
if not kwargs['update_fields'] and kwargs['instance'].id:
# Save it so it can be used in post_save
kwargs['instance'].old = User.objects.get(id=kwargs['instance'].id)
@receiver(post_save, sender=Product)
def update_model(sender, **kwargs):
instance = kwargs['instance']
# Add updated_fields, from old instance, so the method logic remains unchanged
if not kwargs['update_fields'] and hasattr(instance, 'old'):
kwargs['update_fields'] = []
if (kwargs['update_fields'].instance.tax_rate !=
kwargs['update_fields'].instance.old.tax_rate):
kwargs['update_fields'].append('tax_rate')
if 'tax_rate' in kwargs['update_fields']:
update_fields
(if you're not opening Django Admin to the world, this shouldn't be problematic)If you're doing this for many classes, you should probably look at other solutions (but the accepted answer is also not perfect for that!)
Upvotes: 3
Reputation: 3588
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.
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
.
Upvotes: 19
Reputation: 171
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.
Upvotes: 8