Django: base model signal handler doesn't fire - python

In the following sample code:
from django.db import models
from django.db.models.signals import pre_save
# Create your models here.
class Parent(models.Model):
name = models.CharField(max_length=64)
def save(self, **kwargs):
print "Parent save..."
super(Parent, self).save(**kwargs)
def pre_save_parent(**kwargs):
print "pre_save_parent"
pre_save.connect(pre_save_parent, Parent)
class Child(Parent):
color = models.CharField(max_length=64)
def save(self, **kwargs):
print "Child save..."
super(Child, self).save(**kwargs)
def pre_save_child(**kwargs):
print "pre_save_child"
pre_save.connect(pre_save_child, Child)
pre_save_parent doesn't fire when I a Child is created:
child = models.Child.objects.create(color="red")
Is this expected behaviour?

There's an open ticket about this, #9318.
Your workaround looks fine. Here are two others suggested on the ticket by benbest86 and alexr respectively.
Listen on the child class signal, and send the Parent signal there.
def call_parent_pre_save(sender, instance, created, **kwargs):
pre_save.send(sender=Parent, instance=Parent.objects.get(id=instance.id), created=created, **kwargs)
pre_save.connect(call_parent_pre_save, sender=Child)
Do not specify the sender when connecting the signal, then check for subclasses of parent.
def pre_save_parent(sender, **kwargs):
if not isinstance(instance, Parent):
return
#do normal signal stuff here
print "pre_save_parent"
pre_save.connect(pre_save_parent)

I didn't realise sender was an optional parameter to connect. I can do the following:
def pre_save_handler(**kwargs):
instance = kwargs['instance']
if hasattr(instance, 'pre_save'):
instance.pre_save()
pre_save.connect(pre_save_handler)
This allows me to write per Model pre_save methods, and they in turn can call any base class versions (if they exists).
Any better solutions?

Related

Django - call Manager with multiple inherited classes

So I have this class that helps me override the update method of a queryset:
class QuerySetUpdateOverriden(QuerySet, object):
def update(self, *args, **kwargs):
super().update(*args, **kwargs)
if hasattr(self, 'method_from_object'):
self.method_from_object()
return
and here's my class where I use it:
class MyObject:
objects = QuerySetUpdateOverriden.as_manager()
def method_from_object(self):
print("called")
the print statement is never reached.
And I get why - the objects field doesn't inherit MyObject too.
So, the question is - how can I make it inherit MyObject so method_from_object will be called?
Thanks.
You test if self has a method called 'method_from_object', but your QuerySetUpdateOverriden has no method call like this. And MyObject does not inherit from QuerySetUpdateOverriden.
This code would be work i think:
class MyObjectManager(QuerySetUpdateOverriden.as_manager()):
def method_from_object(self):
print("called")
class MyObject(models.Model):
objects = QuerySetUpdateOverriden.as_manager()

Django - how to get all models bound to post_save receivers?

I'm working on a script which finds all Django pre_save and post_save signals and comments overwritten save methods so programmer is aware of all signals affecting the workflow.
For Example
There are two receivers:
#receiver(pre_save, sender=MyModel)
def sig_mymodel_pre_save(instance, sender, **kwargs):
...
#receiver(post_save, sender=MyModel)
def sig_mymodel_post_save(instance, sender, created, **kwargs):
...
And model MyModel:
class MyModel(..):
...
def save(self,*args,**kwargs):
...
super().save(...)
....
I want the script to modify MyModel code to look like this:
class MyModel(..):
...
def save(self,*args,**kwargs):
...
# SIGNAL pre_save | myapp.models.sig_mymodel_pre_save
super().save(...)
# SIGNAL post_save | myapp.models.sig_mymodel_post_save
....
So the first thing I'm going to do is to list all receivers of post_save and pre_save signals:
def get_signals() -> {}:
result = {}
for signal in [pre_save, post_save]:
result[signal] = signal.receivers
return result
def comment_signals_to_save_methods():
for signal, receivers in get_receivers():
for receiver in receivers:
models = ???
And here is the problem - I can't get models for the receiver. There is no such method or attribute.
Do you know how to do it?

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

How to produce a TabularInline that spans models

I have a model (Booking) with a OneToOneField (Thread) that subsequently has a ForeignKey relationship (Message). I would like to show a list of messages on the Booking admin, but with the Thread model in between it appears that this is hard/not possible?
Class Booking(Model):
...
thread = models.OneToOneField('user_messages.Thread', verbose_name='thread')
class Thread(Model):
...
class Message(Model):
thread = models.ForeignKey(Thread, related_name="messages")
Is there a way I can set up my BookingAdmin with an inline that can display messages (spanning across the thread relationship)? Something like:
class MessageInline(TabularInline):
model = Message
fk_name = '???'
class BookingAdmin(ModelAdmin):
inlines = [MessageInline, ]
I'm happy to override the way the Inlines work if that's the best way, but I'm not sure where to tackle that. It looks like overriding *get_formset* might do the trick?
This isn't completely tested yet, but appears to work. The solution is to have an inline and formset with hooks to replace the booking with the attached thread...
class BookingMessageFormset(BaseInlineFormSet):
'''Given a Booking instance, divert to its Thread'''
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
kwargs['instance'] = kwargs['instance'].thread
else:
raise Exception() # TODO Not sure if/when this happens
BaseInlineFormSet.__init__(self, *args, **kwargs)
class MessageInline(admin.TabularInline):
model = Message
formset = BookingMessageFormset
def __init__(self, parent_model, admin_site):
'''Override parent_model'''
super(MessageInline, self).__init__(Thread, admin_site)

Django 1.2: How to connect pre_save signal to class method

I am trying to define a "before_save" method in certain classes in my django 1.2 project. I'm having trouble connecting the signal to the class method in models.py.
class MyClass(models.Model):
....
def before_save(self, sender, instance, *args, **kwargs):
self.test_field = "It worked"
I've tried putting pre_save.connect(before_save, sender='self') in 'MyClass' itself, but nothing happens.
I've also tried putting it at the bottom of the models.py file:
pre_save.connect(MyClass.before_save, sender=MyClass)
I read about connecting signals to class methods here, but can't figure out the code.
Anybody know what I'm doing wrong?
A working example with classmethod:
class MyClass(models.Model):
#....
#classmethod
def before_save(cls, sender, instance, *args, **kwargs):
instance.test_field = "It worked"
pre_save.connect(MyClass.before_save, sender=MyClass)
There's also a great decorator to handle signal connections automatically: http://djangosnippets.org/snippets/2124/
I know this question is old, but I was looking for an answer to this earlier today so why not. It seems from your code that you actually wanted to use an instance method (from the self and the field assignment). DataGreed addressed how to use it for a class method, and using signals with instance methods is pretty similar.
class MyClass(models.Model)
test_field = models.Charfield(max_length=100)
def __init__(self, *args, **kwargs):
super(MyClass, self).__init__(*args, **kwargs)
pre_save.connect(self.before_save, sender=MyClass)
def before_save(self, sender, instance, *args, **kwargs):
self.test_field = "It worked"
I'm not sure if this is a good idea or not, but it was helpful when I needed an instance method called on an object of class A before save from class B.
Rather than use a method on MyClass, you should just use a function. Something like:
def before_save(sender, instance, *args, **kwargs):
instance.test_field = "It worked"
pre_save.connect(before_save, sender=MyClass)

Categories

Resources