Django post_save signal + ManyToManyField: more questions - python

When I tried to connect a handler to model's post_save signal I have found that model's ManyToMany field is empty at that moment. I have googled and found a solution here: ManyToManyField is empty in post_save() function
The solution was to connect to m2m_changed signal of the model.
However I still have some questions.
How to precisely detect if model instance was created and not updated
In the answer there was a condition:
if action == 'post_add' and not reverse:
But it seems to be not working when I am editing the instance in admin interface (seems like m2m field being touched when I am clicking "Save" button in admin).
I have discovered one way to do it via assigning instance attribute in post_save handler
and checking for it in m2m_changed handler.
def on_m2m_changed(sender, instance, action, reverse, *args, **kwargs):
if action == "post_add" and not reverse and instance.just_created:
# do stuff
def on_save(sender, instance, created, *args, **kwargs):
instance.just_created = created
But for me it looks bad and I am not sure that it is the correct way to do that. Is there another way to do it?
What to do if we have multiple m2m fields in the model?
Is order of updating m2m fields of the model well-defined and can we rely on it? Or we should connect to each m2m_changed handler and manipulate flags/counters in instance? BTW, can we rely on the fact that m2m_changed is executed after post_save
May be there is another way to handle complete save of the instance with all its m2m fields?

I have this problem too. Apparently this was a bug (7 years old) and was fixed 3 months ago:
https://code.djangoproject.com/ticket/6707
This might also interests you, in this ticket one of the core developers says this works as intended and won't fix it:
https://code.djangoproject.com/ticket/13022

Related

Pass additional attributes with Django signals

Is there any way to pass additional parameters to instance I'm saving in DB to later access them after the instance is saved?
Brief example of my case:
I'm using Django's signals as triggers to events, like sending a confirmation email, executed by other processes, like workers.
I'm willing to specify which instance and when should trigger the event, and which should not: sometimes I want created/updated records to trigger series of events, and sometimes I want them to be processes silently or do some other actions.
One solution for this is saving desired behaviour for specific instance in model's field like JSONField and recover this behaviour at post_save, but this seems very ugly way of handlign such problem.
I'm using post_save signal as verification that instance was correctly saved in the DB, because I don't want to trigger event and a moment later something goes wrong while saving instance in DB.
Instances are saved through Django Forms, backend routines and RestFramework Seralizers
One solution is to use an arbitrary model instance attribute (not field) to store the desired state. For example:
def my_view(request):
...
instance._send_message = True if ... else False
instance.save()
#receiver(post_save, sender=MyModel)
def my_handler(sender, instance, **kwargs):
if instance._send_message:
...

Do a certain backend operation on clicking Django Admin save button

In a library system, I have the Userbooks (refers to books issued by a user) object registered with Django Admin. And when Django Admin creates a Userbooks object and saves it, the Book object (refers to books in library, also registered with Django Admin) associated with that UserBook with a one_to_one relationship, needs to have its boolean field 'is_issued' to be set to true. How can I do this backend action when Admin clicks the 'save' button?
My suggestion would be to either you use pre save signals or just override the save method to do whatever operation use want
class ModelB(models.Model):
def save(self):
# add logic to change is_issue value to True
super(ModelB, self).save()
Hope this helps.
In the question you specifically asked that this action should happen when the admin tries to save it from admin. The solution suggested by #pansul-bhatt does the same thing on Model save. Even the alternative(Handling pre-save signal) would do the same thing. So even if you save the model from anywhere else in the code you will set is_issued as True.
The better way to do it is to override the save_model on the UserbooksAdmin.
class UserBookAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.is_issued = True
obj.save()
This should be enough to solve your problem. But there are other hooks available with Django Admin.

when to use pre_save, save, post_save in django?

I see I can override or define pre_save, save, post_save to do what I want when a model instance gets saved.
Which one is preferred in which situation and why?
I shall try my best to explain it with an example:
pre_save and post_save are signals that are sent by the model. In simpler words, actions to take before or after the model's save is called.
A save triggers the following steps
Emit a pre-save signal.
Pre-process the data.
Most fields do no pre-processing — the field data is kept as-is.
Prepare the data for the database.
Insert the data into the database.
Emit a post-save signal.
Django does provide a way to override these signals.
Now,
pre_save signal can be overridden for some processing before the actual save into the database happens - Example: (I dont know a good example of where pre_save would be ideal at the top of my head)
Lets say you have a ModelA which stores reference to all the objects of ModelB which have not been edited yet. For this, you can register a pre_save signal to notify ModelA right before ModelB's save method gets called (Nothing stops you from registering a post_save signal here too).
Now, save method (it is not a signal) of the model is called - By default, every model has a save method, but you can override it:
class ModelB(models.Model):
def save(self):
#do some custom processing here: Example: convert Image resolution to a normalized value
super(ModelB, self).save()
Then, you can register the post_save signal (This is more used that pre_save)
A common usecase is UserProfile object creation when User object is created in the system.
You can register a post_save signal which creates a UserProfile object that corresponds to every User in the system.
Signals are a way to keep things modular, and explicit. (Explicitly notify ModelA if i save or change something in ModelB )
I shall think of more concrete realworld examples in an attempt to answer this question better. In the meanwhile, I hope this helps you
pre_save
it's used before the transaction saves.
post_save
it's used after the transaction saves.
You can use pre_save for example if you have a FileField or an ImageField and see if the file or the image really exists.
You can use post_save when you have an UserProfile and you want to create a new one at the moment a new User it's created.
Don't forget about recursions risk.
If you use post_save method with instance.save() calling, instead of .update method, you should disconnect your post_save signal:
Signal.disconnect(receiver=None, sender=None,
dispatch_uid=None)[source] To disconnect a receiver from a signal,
call Signal.disconnect(). The arguments are as described in
Signal.connect(). The method returns True if a receiver was
disconnected and False if not.
The receiver argument indicates the registered receiver to disconnect.
It may be None if dispatch_uid is used to identify the receiver.
... and connect it again after.
update() method don't send pre_ and post_ signals, keep it in mind.

How to use Post_save in Django

I am trying to add points to a User's profile after they submit a comment- using the Django comment framework. I think I need to use a post_save but am not sure to be perfectly honest.
Here is what I have as a method in my models.py:
def add_points(request, Comment):
if Comment.post_save():
request.user.get_profile().points += 2
request.user.get_profile().save()
From the examples of post_save I've found, this is far from what is shown - so I think I am way off the mark.
Thank you for your help.
Unfortunately this makes no sense at all.
Firstly, this can't be a method, as it doesn't have self as the first parameter.
Secondly, it seems to be taking the class, not an instance. You can't save the class itself, only an instance of it.
Thirdly, post_save is not a method of the model (unless you've defined one yourself). It's a signal, and you don't call a signal, you attach a signal handler to it and do logic there. You can't return data from a signal to a method, either.
And finally, the profile instance that you add 2 to will not necessarily be the same as the one you save in the second line, because Django model instances don't have identity. Get it once and put it into a variable, then save that.
The Comments framework defines its own signals that you can use instead of the generic post_save. So, what you actually need is to register a signal handler on comment_was_posted. Inside that handler, you'll need to get the user's profile, and update that.
def comment_handler(sender, comment, request, **kwargs):
profile = request.user.get_profile()
profile.points += 2
profile.save()
from django.contrib.comments.signals import comment_was_posted
comment_was_posted.connect(comment_handler, sender=Comment)

Nullable ForeignKeys and deleting a referenced model instance

I have a ForeignKey which can be null in my model to model a loose coupling between the models. It looks somewhat like that:
class Message(models.Model):
sender = models.ForeignKey(User, null=True, blank=True)
sender_name = models.CharField(max_length=255)
On save the senders name is written to the sender_name attribute. Now, I want to be able to delete the User instance referenced by the sender and leave the message in place.
Out of the box, this code always results in deleted messages as soon as I delete the User instance. So I thought a signal handler would be a good idea.
def my_signal_handler(sender, instance, **kwargs):
instance.message_set.clear()
pre_delete.connect(my_signal_handler, sender=User)
Sadly, it is by no means a solution. Somehow Django first collects what it wants to delete and then fires the pre_delete handler.
Any ideas? Where is the knot in my brain?
Django does indeed emulate SQL's ON DELETE CASCADE behaviour, and there's no out-of-the box documented way to change this. The docs where they mention this are near the end of this section: Deleting objects.
You are right that Django's collects all related model instances, then calls the pre-delete handler for each. The sender of the signal will be the model class about to be deleted, in this case Message, rather than User, which makes it hard to detect the difference between a cascade delete triggered by User and a normal delete... especially since the signal for deleting the User class comes last, since that's the last deletion :-)
You can, however, get the list of objects that Django is proposing to delete in advance of calling the User.delete() function. Each model instance has a semi-private method called _collect_sub_objects() that compiles the list of instances with foreign keys pointing to it (it compiles this list without deleting the instances). You can see how this method is called by looking at delete() in django.db.base.
If this was one of your own objects, I'd recommend overriding the delete() method on your instance to run _collect_sub_objects(), and then break the ForeignKeys before calling the super class delete. Since you're using a built-in Django object that you may find too difficult to subclass (though it is possible to substitute your own User object for django's), you may have to rely on view logic to run _collect_sub_objects and break the FKs before deletion.
Here's a quick-and-dirty example:
from django.db.models.query import CollectedObjects
u = User.objects.get(id=1)
instances_to_be_deleted = CollectedObjects()
u._collect_sub_objects(instances_to_be_deleted)
for k in instances_to_be_deleted.ordered_keys():
inst_dict = instances_to_be_deleted.data[k]
for i in inst_dict.values():
i.sender = None # You will need a more generic way for this
i.save()
u.delete()
Having just discovered the ON DELETE CASCADE behaviour myself, I see that in Django 1.3 they have made the foreign key behaviour configurable.

Categories

Resources