python example How to use Django model inheritance with signals?
django signal providing_args (6)
It's also possible to use content types to discover subclasses - assuming you have the base class and subclasses packaged in the same app. Something like this would work:
from django.contrib.contenttypes.models import ContentType content_types = ContentType.objects.filter(app_label="your_app") for content_type in content_types: model = content_type.model_class() post_save.connect(update_attachment_count_on_save, sender=model)
I have a few model inheritance levels in Django:
class WorkAttachment(models.Model): """ Abstract class that holds all fields that are required in each attachment """ work = models.ForeignKey(Work) added = models.DateTimeField(default=datetime.datetime.now) views = models.IntegerField(default=0) class Meta: abstract = True class WorkAttachmentFileBased(WorkAttachment): """ Another base class, but for file based attachments """ description = models.CharField(max_length=500, blank=True) size = models.IntegerField(verbose_name=_('size in bytes')) class Meta: abstract = True class WorkAttachmentPicture(WorkAttachmentFileBased): """ Picture attached to work """ image = models.ImageField(upload_to='works/images', width_field='width', height_field='height') width = models.IntegerField() height = models.IntegerField()
There are many different models inherited from
WorkAttachment. I want to create a signal, which would update an
attachment_count field for parent work, when attachment is created. It would be logical, to think that signal made for parent sender (
WorkAttachment) would run for all inherited models too, but it does not. Here is my code:
@receiver(post_save, sender=WorkAttachment, dispatch_uid="att_post_save") def update_attachment_count_on_save(sender, instance, **kwargs): """ Update file count for work when attachment was saved.""" instance.work.attachment_count += 1 instance.work.save()
Is there a way to make this signal work for all models inherited from
Python 2.7, Django 1.4 pre-alpha
P.S. I've tried one of the solutions I found on the net, but it did not work for me.
The simplest solution is to not restrict on the
sender, but to check in the signal handler whether the respective instance is a subclass:
@receiver(post_save) def update_attachment_count_on_save(sender, instance, **kwargs): if isinstance(instance, WorkAttachment): ...
However, this may incur a significant performance overhead as every time any model is saved, the above function is called.
I think I've found the most Django-way of doing this: Recent versions of Django suggest to connect signal handlers in a file called
signals.py. Here's the necessary wiring code:
default_app_config = 'your_app.apps.YourAppConfig'
import django.apps class YourAppConfig(django.apps.AppConfig): name = 'your_app' def ready(self): import your_app.signals
def get_subclasses(cls): result = [cls] classes_to_inspect = [cls] while classes_to_inspect: class_to_inspect = classes_to_inspect.pop() for subclass in class_to_inspect.__subclasses__(): if subclass not in result: result.append(subclass) classes_to_inspect.append(subclass) return result def update_attachment_count_on_save(sender, instance, **kwargs): instance.work.attachment_count += 1 instance.work.save() for subclass in get_subclasses(WorkAttachment): post_save.connect(update_attachment_count_on_save, subclass)
I think this works for all subclasses, because they will all be loaded by the time
YourAppConfig.ready is called (and thus
signals is imported).
This solution resolves the problem when not all modules imported into memory.
def inherited_receiver(signal, sender, **kwargs): """ Decorator connect receivers and all receiver's subclasses to signals. @inherited_receiver(post_save, sender=MyModel) def signal_receiver(sender, **kwargs): ... """ parent_cls = sender def wrapper(func): def childs_receiver(sender, **kw): """ the receiver detect that func will execute for child (and same parent) classes only. """ child_cls = sender if issubclass(child_cls, parent_cls): func(sender=child_cls, **kw) signal.connect(childs_receiver, **kwargs) return childs_receiver return wrapper
Michael Herrmann's solution is definitively the most Django-way of doing this. And yes it works for all subclasses as they are loaded at the ready() call.
I would like to contribute with the documentation references :
In practice, signal handlers are usually defined in a signals submodule of the application they relate to. Signal receivers are connected in the ready() method of your application configuration class. If you’re using the receiver() decorator, simply import the signals submodule inside ready().
And add a warning :
The ready() method may be executed more than once during testing, so you may want to guard your signals from duplication, especially if you’re planning to send them within tests.
So you might want to prevent duplicate signals with a dispatch_uid parameter on the connect function.
In this context I'll do :
for subclass in get_subclasses(WorkAttachment): post_save.connect(update_attachment_count_on_save, subclass, dispatch_uid=subclass.__name__)
You could try something like:
model_classes = [WorkAttachment, WorkAttachmentFileBased, WorkAttachmentPicture, ...] def update_attachment_count_on_save(sender, instance, **kwargs): instance.work.attachment_count += 1 instance.work.save() for model_class in model_classes: post_save.connect(update_attachment_count_on_save, sender=model_class, dispatch_uid="att_post_save_"+model_class.__name__)
(Disclaimer: I have not tested the above)
post_save.connect(my_handler, ParentClass) # connect all subclasses of base content item too for subclass in ParentClass.__subclasses__(): post_save.connect(my_handler, subclass)
have a nice day!