Подтвердить что ты не робот

Как использовать наследование модели 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()

Существует много разных моделей, унаследованных от 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

P.S. Я пробовал одно из решений, которое я нашел в сети, но это не сработало для меня.

4b9b3361

Ответ 1

Вы можете попробовать что-то вроде:

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

(Отказ от ответственности: я не тестировал выше)

Ответ 2

Вы можете зарегистрировать обработчик соединения без 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

Ответ 3

Самое простое решение - не ограничивать sender, а проверять обработчик сигнала, является ли соответствующий экземпляр подклассом:

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

Однако это может привести к значительным накладным расходам, поскольку каждый раз, когда сохраняется любая модель, вызывается вышеупомянутая функция.

Я думаю, что нашел самый Django-способ: Последние версии Django предлагают подключить обработчики сигналов в файле с именем signals.py. Здесь необходим код подключения:

your_app/__ __ INIT ру:.

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)

Я думаю, что это работает для всех подклассов, потому что все они будут загружены к тому времени, когда вызывается YourAppConfig.ready (и, следовательно, signals импортируется).

Ответ 4

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)

у нас хороший день!

Ответ 5

Решение Михаэля Херрмана окончательно является самым удобным для Django способом. И да, это работает для всех подклассов, поскольку они загружаются при вызове ready().

Я хотел бы внести вклад с документацией:

На практике обработчики сигналов обычно определяются в сигнальном подмодуле приложения, к которому они относятся. Сигнальные приемники подключены в режиме ready() вашего класса конфигурации приложения. Если вы используете декодер получателя(), просто импортируйте подмодуль сигналов внутри 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

Ответ 6

Это решение разрешает проблему, когда не все модули импортируются в память.

def inherited_receiver(signal, sender, **kwargs):
    """
    Decorator connect receivers and all receiver 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

Ответ 7

Также можно использовать типы контента для обнаружения подклассов - если у вас есть базовый класс и подклассы, упакованные в одно и то же приложение. Что-то вроде этого будет работать:

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)