I have a question regarding the usage of dispatch_uid for signals.
Currently, I am preventing multiple usage of the signal by simply adding if not instance.order_reference. I wonder now if dispatch_uid has the same functionality and I can delete the "if not" clause.
signals.py
def reserveditem_create_order_reference(sender, instance, **kwargs):
if not instance.order_reference:
instance.order_reference = unique_order_reference_generator()
app.py
class OrdersConfig(AppConfig):
name = 'orders'
def ready(self):
#Pre save signal for ReservedItem model
reserved_model = self.get_model('ReservedItem')
pre_save.connect(
reserveditem_create_order_reference,
sender=reserved_model,
dispatch_uid="my_unique_identifier"
)
As by the docs,
In some circumstances, the code connecting receivers to signals may run multiple times. This can cause your receiver function to be registered more than once, and thus called multiple times for a single signal event. If this behavior is problematic (such as when using signals to send an email whenever a model is saved), pass a unique identifier as the dispatch_uid argument to identify your receiver function
So yes, removing the if clause and setting a unique signal receiver dispatch_uid instead will prevent your handlers from being connected (and thereafter, called) more than once.
Related
I have a User model and a UserImage model that contains a foreign key to a User. The foreign key is set to CASCADE delete.
Here is what the receivers look like in my models.py:
#receiver(pre_delete, sender=User)
def deleteFile(sender, instance, **kwargs):
print("User pre_delete triggered")
instance.thumbnail.delete()
#receiver(pre_delete, sender=UserImage)
def deleteFile(sender, instance, **kwargs):
print("UserImage pre_delete triggered")
instance.image.delete()
When I execute the following lines of code:
>>> User.objects.last().delete()
"UserImage pre_delete triggered"
For some reason the associated UserImage signal is being received but the actual User model's signal is not.
Am I missing something?
If you read the documentation carefully you will see that the delete() method on a model will execute purely in SQL (if possible). So the delete() method on UserImage will not be called by Django, thus the signal will not be triggered. If you want it to be triggered you could override the delete method on your User model to also call the delete() on the related object. Something like this:
class User(models.Model):
def delete(self, using=None):
self.userimage_set.all().delete()
super().delete(using=using)
UPDATE:
I did not read the question correctly so I have to update my answer. I think what is happening is that both signals have the same name and thus the first one is overwritten by the second one, and thus only the second one is executed. I would suggest changing the function name to something else and see if that changes things.
I want to trigger some behaviour whenever a many-to-many relationship changes, but I'm unsure what signal setup is best for capturing changes to that relationship that come about due to the deletion of one side of the relationship. m2m_changed does not seem to fire in this case, nor do the regular post_save and post_delete signals seem to be applicable to the through model?
My current solution is to listen to pre_delete on the models which compose either side of the relationship and then clear the relationship within that signal in order to trigger a m2m_changed. I'm surprised that I have to do this though, and feel I've got something wrong.
What am I missing here? And if I am not missing anything why is this necessary (ie why is no such signal fired by default)?
Code example:
class ResearchField(models.Model):
name = models.CharField(unique=True, max_length=200)
class Researcher(models.Model):
name = models.CharField(max_length=200)
research_fields = models.ManyToManyField(ResearchField, blank=True)
#receiver(m2m_changed, sender=Researcher.research_fields.through)
def research_fields_changed(sender, instance, action, **kwargs):
# need to do something important here
print('m2m_changed', action)
volcanology = ResearchField.objects.create(name='Volcanology')
researcher = Researcher.objects.create(name='A. Volcanologist')
researcher.research_fields.add(volcanology)
>>> m2m_changed pre_add
>>> m2m_changed post_add
This m2m_changed signal fires as expected when the relationship is removed from either side:
researcher.research_fields.remove(volcanology)
# or equally volcanology.researcher_set.remove(researcher)
>>> m2m_changed pre_remove
>>> m2m_changed post_remove
However no pre_remove or post_remove m2m_changed signals fire if I simply delete one side of the relationship, despite the Django delete output indicating that an instance of the through model was removed:
# with the relationship intact
volcanology.delete()
>>> (2, {'ResearchField': 1, 'Researcher_research_fields': 1})
At this point I tried:
#receiver(post_delete, sender=Researcher.research_fields.through)
def through_model_deleted(sender, instance, **kwargs):
print('through model deleted')
But this never fires?
As a result my current solution is to have:
#receiver(pre_delete, sender=ResearchField)
def research_field_deleted(sender, instance, **kwargs):
instance.researcher_set.clear()
To custom enforce that when Researcher.research_fields.through objects are being deleted via a cascade from a deleted ResearchField the m2m_changed signal will fire after all. However as I said at the top it feels like I've missed something that this is necessary?
I missed this when posting my question but it seems this is an open bug in Django https://code.djangoproject.com/ticket/17688.
I have written some APIs, for which the respective functions executive inside a transaction block. I am calling the save() method (after some modifications) on instance/s of a/several Model/s, and also consecutively indexing some JSON related information of the instance/s in Elasticsearch. I want the database to rollback even if for some reason the save() for one of the instances or indexing to the Elasticsearch fails.
Now, the problem is arising that even inside the transaction block, the post_save() signals gets called, and that is an issue because some notifications are being triggered from those signals.
Is there a way to trigger post_save() signals only after the transactions have completed successful?
I think the simplest way is to use transaction.on_commit(). Here's an example using the models.Model subclass Photo that will only talk to Elasticsearch once the current transaction is over:
from django.db import transaction
from django.db.models.signals import post_save
#receiver(post_save, sender=Photo)
def save_photo(**kwargs):
transaction.on_commit(lambda: talk_to_elasticsearch(kwargs['instance']))
Note that if the transaction.on_commit() gets executed while not in an active transaction, it will run right away.
Not really. The signals have nothing to do with the db transaction success or failure, but with the save method itself - before the call you have the pre_save signal fired and after the call you have the post_save signal fired.
There are 2 approaches here:
you are going to inspect the instance in the post_save method and decide that the model was saved successfully or not; simplest way to do that: in the save method, after the transaction executed successfully, annotate your instance with a flag, say instance.saved_successfully = True, which you will test in the post_save handler.
you are going to ditch the post_save signal and create a custom signal for yourself, which you will trigger after the transaction ran successfully.
Makes sense?
P.S.
If you strictly need to bind to the transaction commit signal, have a look over this package: https://django-transaction-hooks.readthedocs.org/en/latest/; it looks like the functionality is integrated in Django 1.9a.
I was having serious issues with django's admin not allowing post_save transactions on parent objects when they had inline children being modified.
This was my solution to an error complaining about conducting queries in the middle of an atomic block:
def on_user_post_save_impl(user):
do_something_to_the_user(user)
def on_user_post_save(sender, instance, **kwargs):
if not transaction.get_connection().in_atomic_block:
on_user_post_save_impl(instance)
else:
transaction.on_commit(lambda: on_user_post_save_impl(instance))
We are using this little nugget:
def atomic_post_save(sender, instance, **kwargs):
if hasattr(instance, "atomic_post_save") and transaction.get_connection().in_atomic_block:
transaction.on_commit(lambda: instance.atomic_post_save(sender, instance=instance, **kwargs))
post_save.connect(atomic_post_save)
Then we simply define a atomic_post_save method on any model we like:
class MyModel(Model):
def atomic_post_save(self, sender, created, **kwargs):
talk_to_elasticsearch(self)
Two things to notice:
We only call atomic_post_save when inside a transaction.
It's too late in the flow to send messages and have them included in the current request from inside atomic_post_save.
I'm not sure what this ticket is talking about is what i'm experiencing.
https://code.djangoproject.com/ticket/14051
MyClass.objects.create() calls post_save() handler, and inside one of the handler, I do Myclass.objects.filter(id=instance.id) and it returns nothing.
So when you are inside post_save signal handler,
your instance is not yet found in the DB because it 's not committed yet?
Is this true?
this is create part
thread = ReviewThread.objects.create(**validated_data)
each review_thread has a review_meta (foreign_key) and related_name is 'review_threads'
class ReviewThread(forum_models.Thread):
thread = models.OneToOneField(forum_models.Thread, parent_link=True)
review_meta = models.ForeignKey(ReviewMeta, related_name='review_threads')
This is the receiving part
def maybe_update_review_meta_primary_image(review_thread):
ReviewThread.objects.filter(id=review_thread.id) #returns nothing
#...
#receiver(post_save, sender=ReviewThread)
def update_review_meta(sender, instance, **kwargs):
review_thread = instance
maybe_update_review_meta_primary_image(review_thread)
If you use transactions then yes, it is true. And it is a pretty valid behaviour.
I'm having a hard time to grasp this post_save/pre_save signals from django.
What happens is that my model has a field called status and when a entry to this model is added/saved, it's status must be changed accordingly with some condition.
My model looks like this:
class Ticket(models.Model):
(...)
status = models.CharField(max_length=1,choices=OFFERT_STATUS, default='O')
And my signal handler, configured for pre_save:
def ticket_handler(sender, **kwargs):
ticket = kwargs['instance']
(...)
if someOtherCondition:
ticket.status = 'C'
Now, what happens if I put aticket.save() just bellow this last line if statement is a huge iteration black hole, since this action calls the signal itself. And this problem happens in both pre_save and post_save.
Well... I guess that the capability of altering a entry before (or even after) saving it is pretty common in django's universe. So, what I'm doing wrong here? Is the Signals the wrong approach or I'm missing something else here?
Also, would it be possible to, once this pre_save/post_save function is triggered, to access another model's instance and change a specific row entry on that?
Thanks
Signals are not a proper approach for updating fields in the same model prior to save. Override the model's save method instead of using a signal for this case.
def save(self, force_insert=False, force_update=False):
status = whatever....
super(Ticket, self).save(force_insert, force_update)
For updates to other models, signals are a great approach because you can easily decouple your models. Specifically, you can add a pre_/post_save signal to trigger actions without the need to modify the code of the saved model (which could reside in another application from a third party).
I agree with Carles that this may belong in a save(). When you must do this with signals, make sure you have a very tight condition around the save(). Your test could be rewrtitten as:
if someOtherCondition and ticket.status != 'C':
ticket.status = 'C'
ticket.save()
You won't get into infinite recursion with the test done this way.