web-dev-qa-db-ja.com

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

WorkAttachmentFileBasedWorkAttachmentから継承された多くの異なるモデルがあります。添付ファイルが作成されたときに、親作業の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プレアルファ

P.S.私は試しました ネットで見つけた解決策の1つ ですが、うまくいきませんでした。

42
Silver Light

次のようなことを試すことができます:

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

(免責事項:私は上記をテストしていません)

16
codeape

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

52
Raymond

最も簡単な解決策は、senderを制限するのではなく、それぞれのインスタンスがサブクラスであるかどうかをシグナルハンドラーでチェックすることです。

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

ただし、これにより、everytimeanyモデルとして、パフォーマンスのオーバーヘッドが大幅に増加する可能性があります。保存されると、上記の関数が呼び出されます。

私はこれを行う最もDjangoの方法を見つけたと思います:Djangoの最近のバージョンは、signals.pyというファイルでシグナルハンドラーを接続することを提案しています。必要な配線コードは次のとおりです。

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

Ithinkこれはすべてのサブクラスで機能します。これは、YourAppConfig.readyが呼び出されるまでにすべてがロードされるためです(したがって、signalsがインポートされます)。

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

ごきげんよう!

6
scythargon

Michael Herrmannのソリューションは、間違いなくこれを行うための最もDjangoの方法です。はい、ready()呼び出しでロードされるため、すべてのサブクラスで機能します。

ドキュメントリファレンスを提供したいと思います:

実際には、シグナルハンドラーは通常、関連するアプリケーションのシグナルサブモジュールで定義されます。シグナルレシーバーは、アプリケーション構成クラスのready()メソッドに接続されています。 Receiver()デコレータを使用している場合は、ready()内にsignalsサブモジュールをインポートするだけです。

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

そして警告を追加します:

Ready()メソッドはテスト中に複数回実行される可能性があるため、特にテスト内でシグナルを送信する予定がある場合は、シグナルが重複しないように保護することをお勧めします。

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

そのため、connect関数の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

5
David Kühner

このソリューションは、すべてのモジュールがメモリにインポートされていない場合の問題を解決します。

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
2
beholderrk

私は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モデルと関連するメタクラスを操作するときに必要なようです。

1
clwainwright

基本クラスとサブクラスが同じアプリにパッケージ化されていると仮定して、コンテンツタイプを使用してサブクラスを検出することもできます。このようなものが機能します:

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
Howie