How to use Django model inheritance with signals?

前端 未结 8 728
无人共我
无人共我 2020-12-23 11:15

I have a few model inheritance levels in Django:

class WorkAttachment(models.Model):
    \"\"\" Abstract class that holds all fields that are required in eac         


        
相关标签:
8条回答
  • 2020-12-23 11:51

    You could register the connection handler without sender specified. And filter the needed models inside it.

    from django.db.models.signals import post_save
    from django.dispatch import receiver
    
    
    @receiver(post_save)
    def my_handler(sender, **kwargs):
        # Returns false if 'sender' is NOT a subclass of AbstractModel
        if not issubclass(sender, AbstractModel):
           return
        ...
    

    Ref: https://groups.google.com/d/msg/django-users/E_u9pHIkiI0/YgzA1p8XaSMJ

    0 讨论(0)
  • 2020-12-23 11:55

    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)

    0 讨论(0)
  • 2020-12-23 11:57

    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)
    
    0 讨论(0)
  • 2020-12-23 11:58

    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:

    your_app/__init__.py:

    default_app_config = 'your_app.apps.YourAppConfig'
    

    your_app/apps.py:

    import django.apps
    
    class YourAppConfig(django.apps.AppConfig):
        name = 'your_app'
        def ready(self):
            import your_app.signals
    

    your_app/signals.py:

    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).

    0 讨论(0)
  • 2020-12-23 12:03

    I just did this using python's (relatively) new __init_subclass__ method:

    from django.db import models
    
    def perform_on_save(*args, **kw):
        print("Doing something important after saving.")
    
    class ParentClass(models.Model):
        class Meta:
            abstract = True
    
        @classmethod
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            models.signals.post_save.connect(perform_on_save, sender=cls)
    
    class MySubclass(ParentClass):
        pass  # signal automatically gets connected.
    

    This requires django 2.1 and python 3.6 or better. Note that the @classmethod line seems to be required when working with the django model and associated metaclass even though it's not required according to the official python docs.

    0 讨论(0)
  • 2020-12-23 12:07

    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
    
    0 讨论(0)
提交回复
热议问题