46

我在 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()

WorkAttachmentFileBased从和继承了许多不同的模型WorkAttachment。我想创建一个信号,它会attachment_count在创建附件时更新父工作的字段。认为为父发送者 ( WorkAttachment) 发出的信号也适用于所有继承的模型是合乎逻辑的,但事实并非如此。这是我的代码:

@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()

有没有办法让这个信号适用于所有继承自的模型WorkAttachment

Python 2.7、Django 1.4 pre-alpha

PS 我已经尝试了我在网上找到的一种解决方案,但它对我不起作用。

4

9 回答 9

54

sender您可以在没有指定的情况下注册连接处理程序。并在其中过滤所需的模型。

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

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

于 2013-06-18T16:03:59.407 回答
28

最简单的解决方案是不限制sender,而是在信号处理程序中检查相应的实例是否是子类:

@receiver(post_save)
def update_attachment_count_on_save(sender, instance, **kwargs):
    if isinstance(instance, WorkAttachment):
        ...

但是,这可能会导致显着的性能开销,因为每次保存任何模型时,都会调用上述函数。

我想我找到了最符合 Django 的方法:最近版本的 Django 建议在一个名为signals.py. 这是必要的接线代码:

你的应用程序/__init__.py:

default_app_config = 'your_app.apps.YourAppConfig'

你的应用程序/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)

认为这适用于所有子类,因为它们都将在YourAppConfig.ready调用时加载(因此signals被导入)。

于 2015-03-17T17:53:04.490 回答
15

您可以尝试以下方法:

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

(免责声明:我没有测试过以上)

于 2011-10-17T10:45:32.197 回答
14

我只是使用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.

这需要 django 2.1 和 python 3.6 或更高版本。请注意,@classmethod使用 django 模型和相关元类时似乎需要该行,即使根据官方 python 文档不需要它。

于 2019-02-11T19:45:34.050 回答
8
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)

祝你今天过得愉快!

于 2014-07-08T06:14:27.333 回答
6

Michael Herrmann 的解决方案无疑是最 Django 的做法。是的,它适用于所有子类,因为它们是在 ready() 调用中加载的。

我想为文档参考做出贡献:

在实践中,信号处理程序通常定义在与它们相关的应用程序的信号子模块中。信号接收器连接在应用程序配置类的 ready() 方法中。如果您使用的是 receiver() 装饰器,只需在 ready() 中导入信号子模块。

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

并添加警告:

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

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

因此,您可能希望在连接函数上使用 dispatch_uid 参数来防止重复信号。

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

在这种情况下,我会做:

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

于 2015-07-14T09:01:30.393 回答
2

此解决方案解决了并非所有模块都导入内存时的问题。

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
于 2014-04-18T11:35:41.990 回答
0

也可以使用内容类型来发现子类——假设您将基类和子类打包在同一个应用程序中。像这样的东西会起作用:

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)
于 2013-05-18T22:03:16.893 回答
0

除了@clwainwright 答案之外,我还配置了他的答案以改为适用于 m2m_changed 信号。我必须将其发布为代码格式的答案才有意义:

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

如果在未来的 Django 版本中发生任何变化,它会进行一些检查以确保它不会中断。

于 2021-01-18T21:08:48.170 回答