Django signals по-новому

08 Sep 2008


На пути к 1.0 релизу Django претерпевал немало радикальных изменений. Одно из них рефакторинг системы сигналов.

Если вы первый раз читаете и не в курсе «что это такое и с чем его едят», то скажу в двух словах. Это система реагирования на события приложения. Любой JavaScript или прикладной UI программист хорошо знаком с системой событий (event) — клик мышкой, нажатие горячей клавиши и т.п. Для программистов серверной части веба все выглядит немного по другому. Есть HTTP-запрос и есть его обработчик, анализируется как правило URL на предмет «кому отправлять запрос». Но на самом деле это та же система сигналов-событий, только узкопрофилированная под обработку HTTP-запросов.

Оказывается серверная часть веб-приложения тоже может, и я уверен, просто должна генерировать намного больший спектр сигналов, чем просто обработку URL и данных запроса. С чем успешно и справляется Django. Теперь немного прозы. Какие же события может ловить Django «из-коробки».

  • pre_init — перед запуском метода-конструктора __init__() модели;
  • post_init — после выполнения метода __init__() модели;
  • pre_save — перед сохранением экземпляра модели в базу;
  • post_save — после успешного сохранения экземпляра модели в базу;
  • pre_delete, post_delete — перед и после удаления экземпляра модели из базы;
  • post_syncdb — генерируется админкой Django после установки нового приложения (INSTALLED_APPS);
  • request_started, request_finished, got_request_exception — генерируются при обработке HTTP-запросов;
  • template_rendered — генерируется только в режиме тестирования приложения.

Итак сигналы *_init, _*save, *_delete и post_syncdb генерируются системой моделей Django и их можно импортировать из django.db.models.signals. Сигналы обработки HTTP-запросов из django.core.signals. И тестовый template_rendered из django.test.signals.

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

Код модели:

class MimeType(models.Model):
    """
    Mime Types table
    """
    name = models.CharField(max_length=200)
    slug = models.SlugField()

    def __unicode__(self):
        return self.name

class Item(models.Model):
    """
    Main file
    """
    name = models.CharField(max_length=200)
    slug = models.SlugField()
    file = models.FileField(upload_to='files', blank=True)
    mime = models.ForeignKey(MimeType, blank=True, null=True)
    upload_date = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return self.name

    def set_mime(self, mime):
        obj, created = MimeType.objects.get_or_create(name=mime, slug=slugify(mime))
        self.mime = obj

Посмотрите на метод set_mime модели Item. Он создает новый объект MimeType или использует если он уже создан. При этом не сохраняя модель Item. Теперь я пишу функцию-callback, которая будет вызываться по событию сохранения модели Item.

import mimetypes
import os.path

# init mime types dict
mimetypes.init()

def add_mime_type(instance, **kwargs):
    """
    Adding mime-type to uploaded file (for future use).
    Would be called on post-save.
    """
    if not hasattr(instance, 'mime') or not hasattr(instance, 'file'):
        raise Exception("Object %s does not have 'mime' attribute! Can't set mime-type!" % instance)
    if instance.file:
        extension = os.path.splitext(instance.file.path)[1].lower()
        instance.set_mime(mimetypes.types_map[extension])

Итак, функция-callback add_mime_type принимает параметр instance, который является экземпляром модели, сгенерировавшим сигнал (в нашем случае модели Item). Вторым параметром принимает словарь (dictionary). Кстати, это и есть один из моментов обратной несовместимости. После рефакторинга каждая функция-callback должна принимать параметр **kwargs.

Теперь третий самый простой шаг — связывание модели с сигналом. В самом конце файла моделей добавилась строчка:

models.signals.pre_save.connect(add_mime_type, sender=Item)

Теперь перед каждым сохранением объектов Item, будет выполняться проверка и установка Mime-типа.

Кстати, я советую если у вас больше одного сигнала, выносите их в отдельный файл signals.py или можно по-старинке писать в файле моделей (что ИМХО иногда мешает чтению кода).

Почитать про сигналы можно еще в официальной доке: Django Signals и Django Built-in signal reference. Почитать про рефаторинг можно на странице Backwards Incompatible Changes и в соответствующем коммите 8223.

Александр Кошелев тоже немного раньше написал по теме, статья «А вы поймали новые сигналы?» с хорошим примером создания абстрактного сигнала.

«На сегодня все. Вопросы в студию» :-)