简体   繁体   English

如何将 Django 模型继承与信号一起使用?

[英]How to use Django model inheritance with signals?

I have a few model inheritance levels in Django:我在 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 WorkAttachmentFileBased and WorkAttachment .WorkAttachmentFileBasedWorkAttachment继承了许多不同的模型。 I want to create a signal, which would update an attachment_count field for parent work, when attachment is created.我想创建一个信号,它会在创建attachment_count更新父工作的attachment_count字段。 It would be logical, to think that signal made for parent sender ( WorkAttachment ) would run for all inherited models too, but it does not.认为为父发件人( WorkAttachment )制作的信号也适用于所有继承模型是合乎逻辑的,但事实并非如此。 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 WorkAttachment ?有没有办法让这个信号适用于从WorkAttachment继承的所有模型?

Python 2.7, Django 1.4 pre-alpha Python 2.7,Django 1.4 pre-alpha

PS I've tried one of the solutions I found on the net , but it did not work for me. PS 我已经尝试了我在网上找到的一种解决方案,但它对我不起作用。

You could register the connection handler without sender specified.您可以在不指定sender情况下注册连接处理程序。 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参考: https : //groups.google.com/d/msg/django-users/E_u9pHIkiI0/YgzA1p8XaSMJ

The simplest solution is to not restrict on the sender , but to check in the signal handler whether the respective instance is a subclass:最简单的解决方案是不限制sender ,而是在信号处理程序中检查相应的实例是否是子类:

@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 .我想我已经找到了最 Django 的方式来做到这一点:Django 的最新版本建议将信号处理程序连接到一个名为signals.py的文件中。 Here's the necessary wiring code:这是必要的接线代码:

your_app/__init__.py: your_app/__init__.py:

default_app_config = 'your_app.apps.YourAppConfig'

your_app/apps.py: 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: 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).认为这适用于所有子类,因为它们将在YourAppConfig.ready被调用时加载(因此signals被导入)。

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) (免责声明:我没有测试以上内容)

I just did this using python's (relatively) new __init_subclass__ method :我只是使用 python 的(相对)新的__init_subclass__方法做到了这一点:

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.这需要 django 2.1 和 python 3.6 或更高版本。 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.请注意, @classmethod行在使用 django 模型和关联的元类时似乎是必需的,即使根据官方 python 文档它不是必需的。

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!祝你今天过得愉快!

Michael Herrmann's solution is definitively the most Django-way of doing this. Michael Herrmann 的解决方案绝对是最 Django 的方式。 And yes it works for all subclasses as they are loaded at the ready() call.是的,它适用于所有子类,因为它们在 ready() 调用中加载。

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.信号接收器在应用程序配置类的 ready() 方法中连接。 If you're using the receiver() decorator, simply import the signals submodule inside ready().如果您使用receiver() 装饰器,只需在ready() 中导入信号子模块。

https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions

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. ready() 方法在测试期间可能会执行多次,因此您可能希望防止信号重复,特别是如果您计划在测试中发送它们。

https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions https://docs.djangoproject.com/en/dev/topics/signals/#connecting-receiver-functions

So you might want to prevent duplicate signals with a dispatch_uid parameter on the connect function.因此,您可能希望在连接函数上使用 dispatch_uid 参数来防止重复信号。

post_save.connect(my_callback, dispatch_uid="my_unique_identifier")

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

https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals

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

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)

In addition to @clwainwright answer, I configured his answer to instead work for the m2m_changed signal.除了@clwainwright 的回答之外,我还配置了他的回答,以便为 m2m_changed 信号工作。 I had to post it as an answer for the code formatting to make sense:我不得不将其作为代码格式的答案发布才能有意义:

@classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for m2m_field in cls._meta.many_to_many:
            if hasattr(cls, m2m_field.attname) and hasattr(getattr(cls, m2m_field.attname), 'through'):
                models.signals.m2m_changed.connect(m2m_changed_receiver, weak=False, sender=getattr(cls, m2m_field.attname).through)

It does a couple of checks to ensure it doesn't break if anything changes in future Django versions.它会进行一些检查以确保在未来的 Django 版本中发生任何变化时它不会中断。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM