Александр Кошелев
Александр Кошелев Python-разработчик

M2M отношение и post_save сигнал

Уже давно известно о небольшой(?) проблеме, связанной с post_save сигналом и ManyToMany отношением моделей. Для примера, Лориен столкнулся некоторое время назад с ней, о чем есть интересный тред на форуме.

Я же всё как-то раньше мимо проходил этой особенности, но тут столкнулся лицом к лицу. Кратко расскажу предысторию.

В процессе уменьшения TODO листа по улучшению блога, встала задача организации оповещения по почте о новых комментариях. Но делать реализацию жестко завязанную на блоге не хотелось, поскольку задача оповещения в принципе может встать в любом приложении. Поэтому некоторое время я пытался разработать, до некоторой степени, универсальную систему оповещений, легко интегрируемую в любое приложение. Одной из особенностей этой системы была завязка на джанго сигналах. Уж больно направилась такая связь: произошло некоторые событие -> образовался сигнал -> пошло оповещение нужным пользователям.

И когда черновой вариант системы был реализован, я сразу решил бросить ей в бой. Первый вариант не сработал. Повесить оповещение на post_save сигнал у модели Comment не получилось. Точнее получилось конечно, но не был достигнут нужный результат. Модели связаны схематично вот так:

class Comment( models.Model ):
    #...

class Post( models.Model ):
    comments = models.ManyToManyField( Comment )
    #...

В обработчике сигнала я должен был уже получить ссылку на пост, к которому создан комментарий, но не вышло. Сигнал шел раньше чем m2m зависимость между постом и комментарием сохранялась.

Просто решения я не нашел. И было принято волевое решение(:)) слегка доработать ManyToManyField. Точнее отнаследоваться от него и немного по-шаманить. Шаманство заключается в подмене метода add у m2m менеджера. Для этого был написан вот такой код:

def make_add_method( parent ):
    def ext_add( self, *objs ):
        parent(*objs)
        dispatcher.send( signal=signals.m2m_post_save, sender=self.model, instance=self.instance, objects=objs)
    ext_add.alters_data = True
    return ext_add
    
class ExtManyRelatedObjectsDescriptor(related.ManyRelatedObjectsDescriptor):
    def __get__(self, instance, instance_type=None):
        manager = super( ExtManyRelatedObjectsDescriptor, self ).__get__( instance, instance_type )
        manager.__class__.add = make_add_method( manager.add )
        return manager

class ExtReverseManyRelatedObjectsDescriptor(related.ReverseManyRelatedObjectsDescriptor ):
    def __get__(self, instance, instance_type=None):
        manager = super( ExtReverseManyRelatedObjectsDescriptor, self ).__get__( instance, instance_type )
        manager.__class__.add = make_add_method( manager.add )
        return manager

class ExtManyToManyField( related.ManyToManyField ):
    def contribute_to_class(self, cls, name):
        super(ExtManyToManyField, self).contribute_to_class(cls, name)
        # Add the descriptor for the m2m relation
        setattr(cls, self.name, ExtReverseManyRelatedObjectsDescriptor(self))

    def contribute_to_related_class(self, cls, related):
        super(ExtManyToManyField, self).contribute_to_related_class(cls, related)
        # m2m relations to self do not have a ManyRelatedObjectsDescriptor,
        # as it would be redundant - unless the field is non-symmetrical.
        if related.model != related.parent_model or not self.rel.symmetrical:
            # Add the descriptor for the m2m relation
            setattr(cls, related.get_accessor_name(), ExtManyRelatedObjectsDescriptor(related))

Он не очень прозрачен, но своё дело делает. Немного поясню. m2m_post_save это мною определенный сигнал специально для этого случая. Он посылается уже после того как объект был добавлен в m2m таблицу. В качестве дополнительных параметров(помимо sender) передается объект к которому присоединяется объект сендер (в моём случае этот объект - пост) и последовательность присоединенных сендоров(в моём случае - комментарии).

Для использования этого поля, пришлось немного поменять исходные модули. Но на такую жертву я готов был пойти:)

#...
comments = ExtManyToManyField( Comment )

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

А как вы считаете, не слишком корявое получилось решение?

PS: Кстати, оповещения уже работают в тестовом режиме. Так что можете подписываться на обновления в комментариях. Если какие-то будут ошибки, очень прошу вас мне о них сообщить. Буду очень благодарен.

comments powered by Disqus