Django - Pass value from pre_save to post_save methods on model - python

I've got a Django model like so...
class Example(models.Model):
title = models.CharField(...)
...
I'm trying to compare two values - the title field before the user changes it, and the title after. I don't want to save both values in the database at one time (only need one title field), so I'd like to use pre_save and post_save methods to do this. Is it possible to get the title before the save, then hold this value to be passed into the post_save method?
The pre_save and post_save methods look like so...
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the current title name here
x = instance.title
#receiver(post_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the new title name here and compare the difference
x = instance.title # <- new title
if x == old_title_name: # <- this is currently undefined, but should be retrieved from the pre_save method somehow
...do some logic here...
Any ideas would be greatly appreciated!
Edit
As was pointed out to me, pre_save and post_save both occur after save() is called. What I was looking for is something like pre_save() but before the actual save method is called. I set this on the model so that the logic to be performed will be accessible wherever the instance is saved from (either admin or from a user view)

Use Example.objects.get(pk=instance.id) to get the old title from the database in the pre_save handler function:
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
new_title = instance.title # this is the updated value
old_title = Example.objects.get(pk=instance.id)
# Compare the old and new titles here ...
This trick was proposed here a long time ago. I've not tested it with the recent Django version. Please let me know whether it's still working.

We can only say object has changed if "save" method passes successfully, so post_save is good to be sure that model object has updated.
Setting on the fly attribute on model class instance can do the task, as the same instance is passed from pre_save to post_save.
def set_flag_on_pre_save(sender, instance, *args, **kwargs):
# Check here if flag setting condition satisfies
set_the_flag = true
if set_the_flag:
instance.my_flag=0
def check_flag_on_post_save(sender, instance, *args, **kwargs):
try:
print(instance.my_flag)
print('Flag set')
except AttributeError:
print('Flag not set')
pre_save.connect(set_flag_on_pre_save, sender=ModelClass)
post_save.connect(check_flag_on_post_save, sender=ModelClass)

Related

on_save() is not triggered when model is created

I got the two following models:
class Stuff(models.Model):
...
def custom_function(self):
...
class MyModel(models.Model):
name = models.CharField(max_length=200)
many_stuff = models.ManyToManyField(Stuff, related_name="many_stuff+")
many_other_stuff = models.ManyToManyField(Stuff, related_name="many_other_stuff+")
def __init__(self, *args, **kwargs):
super(MyModel, self).__init__(*args, **kwargs)
for stuff in self.many_stuff.all():
many_stuff.custom_function()
def on_save(self, *args, **kwargs):
for stuff in self.many_stuff.all():
many_stuff.custom_function()
super(MyModel, self).save(*args, **kwargs)
When I create a new instance of MyModel through the Django Admin, I want to execute for each Stuff object, a function. It works on_save but I cannot make it work on __init__ for some reason. If I create the new instance, the functions don't get executed. If I save the newly created instance, the functions do get executed.
To be more specific, when I use this __init__ method, the model breaks on instance creation with the error:
MyModel needs to have a value for field "id" before this many-to-many relationship can be used.
I also attempted doing the same thing with the post_save signal, instead of using __init__ and on_save but I had the same problem. On object creation, the function custom_function did not get executed, but it did when I saved the object after it has been created by clicking on the save button.
Any ideas?
You simply cant create M2M join while creating object.
Because M2M model fields don't have real db fields, but have intermediary table that stores id from both model. When you create relation between two objects, both objects must have an id value ready to use.
First create MyModel instance without M2M fields, than set M2M fields. Something like that;
newModel = MyModel.objects.create(name=someting)
newStuff = Stuff.objects.create(..)
newModel.many_stuff.add(newStuff)

Django: Is there a way to cancel an receiver triggering for a particular model signal?

I have a post_save signal receiver for a model in my domain. This receiver is triggered by many routines that run a save on that model (therefore I can't delete that receiver yet).
#receiver(signal=post_save, sender=OrderGroup)
def check_commission_should_be_synced(sender, instance, created, **kwargs):
# Receiver procedure
# ...
I would like to cancel its triggering for a particular method that manipulates my model. Is that possible?
I'm using Django 1.7 with Python 2.7.
Add a non-database Boolean attribute to your model defaulting to False, like:
class MyModel(models.Model):
# existing datanase fields
trigger_post_save = False
and for the methods you don't want to trigger post_save, set it to True before save:
my_instance.trigger_post_save = True
my_instance.save()
Finally, in your decorated method check the value and return if it's set:
#receiver(signal=post_save, sender=OrderGroup)
def check_commission_should_be_synced(sender, instance, created, **kwargs):
if instance.trigger_post_save:
return
# the rest of code

Identify the changed fields in django post_save signal

I'm using django's post_save signal to execute some statements after saving the model.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# do some stuff
pass
Now I want to execute a statement based on whether the value of the mode field has changed or not.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# if value of `mode` has changed:
# then do this
# else:
# do that
pass
I looked at a few SOF threads and a blog but couldn't find a solution to this. All of them were trying to use the pre_save method or form which are not my use case. https://docs.djangoproject.com/es/1.9/ref/signals/#post-save in the django docs doesn't mention a direct way to do this.
An answer in the link below looks promising but I don't know how to use it. I'm not sure if the latest django version supports it or not, because I used ipdb to debug this and found that the instance variable has no attribute has_changed as mentioned in the below answer.
Django: When saving, how can you check if a field has changed?
If you want to compare state before and after save action, you can use pre_save signal which provide you instance as it should become after database update and in pre_save you can read current state of instance in database and perform some actions based on difference.
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=MyModel)
def on_change(sender, instance: MyModel, **kwargs):
if instance.id is None: # new object will be created
pass # write your code here
else:
previous = MyModel.objects.get(id=instance.id)
if previous.field_a != instance.field_a: # field will be updated
pass # write your code here
Ussually it's better to override the save method than using signals.
From Two scoops of django:
"Use signals as a last resort."
I agree with #scoopseven answer about caching the original value on the init, but overriding the save method if it's possible.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
__original_mode = None
def __init__(self, *args, **kwargs):
super(Mode, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.mode != self.__original_mode:
# then do this
else:
# do that
super(Mode, self).save(force_insert, force_update, *args, **kwargs)
self.__original_mode = self.mode
UPDATE IF YOU NEED SIGNALS
Just in case you really need signals because you need a decoupled app or you can't simply override the save() method, you can use pre_save signal to 'watch' previous fields
#receiver(pre_save, sender=Mode)
def check_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
if instance.mode != original_mode:
# then do this
else:
# do that
The problem with this is that you make changes before, so if save() has a problem you could have some issues later.
So to fix that issue, you can store the original value on the pre_save and use on post_save.
#receiver(pre_save, sender=Mode)
def cache_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
instance.__original_mode = original_mode:
#receiver(post_save, sender=Mode)
def post_save_mode_handler(sender, instance, created, **kwargs):
if instance.__original_mode != instance.original_mode:
# then do this
else:
# do that
The problem with signals and this approach also is that you need one more query to check previous values.
Set it up on the __init__ of your model so you'll have access to it.
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
Now you can perform something like:
if instance.mode != instance.__original_mode:
# do something useful
This is an old question but I've come across this situation recently and I accomplished it by doing the following:
class Mode(models.Model):
def save(self, *args, **kwargs):
if self.pk:
# If self.pk is not None then it's an update.
cls = self.__class__
old = cls.objects.get(pk=self.pk)
# This will get the current model state since super().save() isn't called yet.
new = self # This gets the newly instantiated Mode object with the new values.
changed_fields = []
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
changed_fields.append(field_name)
except Exception as ex: # Catch field does not exist exception
pass
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
This is more effective since it catches all updates/saves from apps and django-admin.
in post_save method you have kwargs argument that is a dictionary and hold some information. You have update_fields in kwargs that tell you what fields changed. This fields stored as forzenset object. You can check what fields changed like this:
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
if not created:
for item in iter(kwargs.get('update_fields')):
if item == 'field_name' and instance.field_name == "some_value":
# do something here
But there is an issue in this solution. If your field value for example was 10, and you update this field with 10 again, this field will be in update_fields again.
I'm late but it can be helpful for others.
We can make custom signal for this.
Using custom signal we can easily do these kind of things:
Post is created or not
Post is modified or not
Post is saved but any field does not changed
class Post(models.Model):
# some fields
Custom signals
**Make signal with arguments **
from django.dispatch import Signal, receiver
# provide arguments for your call back function
post_signal = Signal(providing_args=['sender','instance','change','updatedfields'])
Register signal with call back function
# register your signal with receiver decorator
#receiver(post_signal)
def post_signalReciever(sender,**kwargs):
print(kwargs['updatedfields'])
print(kwargs['change'])
Sending the signal from post-admin
We sending the signals from Post admin and also save object when it actually modified
#sending the signals
class PostAdmin(admin.ModelAdmin):
# filters or fields goes here
#save method
def save_model(self, request, obj, form, change):
if not change and form.has_changed(): # new post created
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post created')
elif change and form.has_changed(): # post is actually modified )
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post modified')
elif change and not form.has_changed() :
print('Post not created or not updated only saved ')
See also:
Django Signals official doc
This can be identified using instance._state.adding
if not instance._state.adding:
# update to existing record
do smthng
else:
# new object insert operation
do smthng
You can use update_fields in django signals.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# only update instance
if not created:
update_fields = kwargs.get('update_fields') or set()
# value of `mode` has changed:
if 'mode' in update_fields:
# then do this
pass
else:
# do that
pass

Django: Auto Instantiation of custom primary key?

I have a class with a custom pk field, and a classmethod to generate this special pk:
class Card(models.Model):
custom_pk = models.BigIntegerField(primary_key=True)
other_attr = ...
#classmethod
def gen_id(cls):
# ...
return the_id
Now, I suppose I can create object (in a view for example) doing this:
Card.objects.create(custom_pk=Card.gen_id(), other_attr="foo")
But I'd like to have the same result using the classic way to do it:
Card.objects.create(other_attr="foo")
Thanks!
You can use pre_save signal to supply your primary key is missing. This signal handler will be called before each call to Card.save() method, therefore we need to make sure that we won't override custom_pk if already set:
#receiver(pre_save, sender=Card)
def add_auto_pk(sender, instance, **kwargs):
if not instance.custom_pk:
instance.custom_pk = Card.get_id()
See Django signal documentation for more details.

Django Model Field Default Based Off Another Field in Same Model

I have a model that I would like to contain a subjects name and their initials (he data is somewhat anonymized and tracked by initials).
Right now, I wrote
class Subject(models.Model):
name = models.CharField("Name", max_length=30)
def subject_initials(self):
return ''.join(map(lambda x: '' if len(x)==0 else x[0],
self.name.split(' ')))
# Next line is what I want to do (or something equivalent), but doesn't work with
# NameError: name 'self' is not defined
subject_init = models.CharField("Subject Initials", max_length=5, default=self.subject_initials)
As indicated by the last line, I would prefer to be able to have the initials actually get stored in the database as a field (independent of name), but that is initialized with a default value based on the name field. However, I am having issues as django models don't seem to have a 'self'.
If I change the line to subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), I can do the syncdb, but can't create new subjects.
Is this possible in Django, having a callable function give a default to some field based on the value of another field?
(For the curious, the reason I want to separate my store initials separately is in rare cases where weird last names may have different than the ones I am tracking. E.g., someone else decided that Subject 1 Named "John O'Mallory" initials are "JM" rather than "JO" and wants to fix edit it as an administrator.)
Models certainly do have a "self"! It's just that you're trying to define an attribute of a model class as being dependent upon a model instance; that's not possible, as the instance does not (and cannot) exist before your define the class and its attributes.
To get the effect you want, override the save() method of the model class. Make any changes you want to the instance necessary, then call the superclass's method to do the actual saving. Here's a quick example.
def save(self, *args, **kwargs):
if not self.subject_init:
self.subject_init = self.subject_initials()
super(Subject, self).save(*args, **kwargs)
This is covered in Overriding Model Methods in the documentation.
I don't know if there is a better way of doing this, but you can use a signal handler for the pre_save signal:
from django.db.models.signals import pre_save
def default_subject(sender, instance, using):
if not instance.subject_init:
instance.subject_init = instance.subject_initials()
pre_save.connect(default_subject, sender=Subject)
Using Django signals, this can be done quite early, by receiving the post_init signal from the model.
from django.db import models
import django.dispatch
class LoremIpsum(models.Model):
name = models.CharField(
"Name",
max_length=30,
)
subject_initials = models.CharField(
"Subject Initials",
max_length=5,
)
#django.dispatch.receiver(models.signals.post_init, sender=LoremIpsum)
def set_default_loremipsum_initials(sender, instance, *args, **kwargs):
"""
Set the default value for `subject_initials` on the `instance`.
:param sender: The `LoremIpsum` class that sent the signal.
:param instance: The `LoremIpsum` instance that is being
initialised.
:return: None.
"""
if not instance.subject_initials:
instance.subject_initials = "".join(map(
(lambda x: x[0] if x else ""),
instance.name.split(" ")))
The post_init signal is sent by the class once it has done initialisation on the instance. This way, the instance gets a value for name before testing whether its non-nullable fields are set.
As an alternative implementation of Gabi Purcaru's answer, you can also connect to the pre_save signal using the receiver decorator:
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=Subject)
def default_subject(sender, instance, **kwargs):
if not instance.subject_init:
instance.subject_init = instance.subject_initials()
This receiver function also takes the **kwargs wildcard keyword arguments which all signal handlers must take according to https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions.

Categories

Resources