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

Одна из таких подсистем в Django, к которой я не особенно сразу проникся — манипуляторы. В начале мне было не очень понятно их место в общей архитектуре, и они казались чем-то лишним, что требует слишком много усилий, а взамен дает слишком мало удовольствия. Я был не прав! Я просто "не умел их готовить"...

Проблемы автоматических манипуляторов

Позаимствую картинку из одной из предыдущих статей, чтобы напомнить, что такое манипулятор. Это объект, который содержит список полей, знает, какими элементами отображать их в HTML-форме, знает, в каком виде из HTML приходят их данные, как их валидировать, как их конвертировать в Питоновские типы и как сохранять в БД. То есть сильно автоматизирует редактирование объекта в HTML.

Пишу "сильно автоматизирует", а не "полностью реализует", потому что кое-что надо делать вручную: например рисовать шаблон самой формы, а также обрабатывать сохранение файлов.

Самое приятное то, что для ваших моделей манипуляторы создаются автоматически и имеют список полей, соответствующий полям модели, и с нужными валидаторами. Таким образом, в простых случаях для создания и редактирования модели можно вообще не писать кода для показа и обработки форм, а сказать Django использовать автоматический манипулятор, нарисовать шаблон, и написать, по каким URL это делать. Это все хорошо описано в документации про generic views, в разделе про create/update/delete.

Проблемы начинаются там, где случаи перестают быть совсем простыми. Так сложилось, что в моем первом проекте их практически не было, все формы были чуть-чуть, но с каким-то вывертом. Была форма, которая должна была одинаково выглядеть, но по-разному работать в зависимости от того, обрабатывает она один выделенный объект или группу. Была форма сохранения данных клиента, которая должна была не менять его пароль. И была форма изменения пароля, которая должна была отображать не одно только поле пароля, а два для подтверждения ввода. Вещи, в общем-то, не убиться, какие сложные, но автоматические манипуляторы для них не работали.

Официальная документация рекомендует в таких случаях писать собственные манипуляторы взамен автоматических. Здесь весь список полей, их опции и валидаторы к ним задаются вручную. И если даже вас не устраивает поведение одного поля из десяти, вручную придется создать все. Это очень нудно и скучно, потому что это такой copy-paste каждого поля с небольшими изменениями синтаксиса, причем в двух местах: во-первых в конструкторе для каждого поля модели надо создать поле манипулятора с подходящим типом, тем же именем, и такими же валидаторами, во-вторых в методе сохранения надо данные каждого поля назначить в объект модели. Но самое плохое, что при каждом изменении модели придется бегать по коду и делать адекватные копии. А это как раз то, от чего Django со своим принципом DRY по идее должен программиста избавлять.

Практические приемы

По прошествии времени, понаблюдав за чужим кодом и покопавшись в коде самого Django, я усвоил несколько приемов, которые как раз решают проблемы этих очень частых случаев, когда все "одинаково, но не совсем". Это не хаки, а вполне нормальные подходы, просто из документации они не очевидны (по крайней мере, не были очевидны для меня).

Нередактируемые поля

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

class Album(models.Model):
  ...
  download_count = models.PositiveIntegerField(editable=False)

editable=False заставит автоматический манипулятор не обращать внимания на поле.

Подмена контролов

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

class User(models.Model):
  ...
  rights = models.ManyToManyField(Right)

Для такого поля в автоматическом манипуляторе создается <select multiple>, но нам хочется, чтобы это представлялось набором из <input type="checkbox">. Тогда надо создать свой манипулятор, но не с нуля, а в виде наследника от автоматического. И в конструкторе выкинуть стандартное поле и вставить вместо него свое:

from myproject.myapp.models import User
from django import forms

class UserManipulator(User.ChangeManipulator):
  def __init__(self, id):

    # Вызов унаследованного конструктора
    User.ChangeManipulator.__init__(self, id)

    # Поиск и удаление стандартного поля
    for field in self.fields:
      if field.field_name == 'rights':
        self.fields.remove(field)
        break

    # Добавление своего поля
    choices = self.original_object._meta.get_field('rights').get_choices_default()
    self.fields.append(forms.CheckboxSelectMultipleField(
      'rights',
      choices=choices))

Теперь представьте, что такой вот кошмар вроде поиска всего-навсего списка выбора прав пришлось бы делать не для одного поля, а для всех в случае полностью ручного манипулятора :-).

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

Follow

Бывает, что объект редактируется частично в одной форме, частично — в другой. В том же предыдущем примере с пользователем список его прав редактируется в интерфейсе администратора, а всевозможные личные данные редактируются в профайле пользователя им самим. В этом случае нужно, чтобы манипулятор формы обращал внимание только на "свои" поля, причем editable=False тут не поможет, потому что он исключит поле из всех манипуляторов.

Для этого нужно использовать недокументированный параметр к конструктору — follow:

# Для административной формы
manipulator = User.ChangeManipulator(id, follow={
  'birth_date': False,
  'country': False,
  'city': False,
})

# Для формы в профайле
manipulator = User.ChangeManipulator(id, follow={
  'rights': False,
})

Словарь "follow" есть в любом манипуляторе и он показывает, какие поля манипулятор обрабатывает: только те, которые в нем есть, и у которых значение не "false". В автоматическом манипуляторе follow заполняется всеми editable-полями, а потом обновляется из переданного в параметре конструктора. Таким образом можно отключать отдельные поля из редактирования.

А можно и включать. Не берусь привести сейчас пример, но помню, что был случай, когда мне надо было включить какое-то по умолчанию отключенное поле, передав follow={'fieldname': True}.

Паттерн

Манипуляторы годятся не только для редактирования объектов из БД. Ими можно обрабатывать, в общем-то, любые формы. Например, форму логина, форму регистрации, форму высылки email'а с забытым паролем. Для этого стоит создать полностью свой манипулятор, у которого будут нужные поля, а вместо метода save() — подходящий: authorize(), rigester(), send_password().

Здесь очень помогает, что у Django есть целая библиотека валидаторов общего назначения, которые могут проверять уйму распространенных условий. Например класс AlwaysMatchesOtherField идеально подходит для формы с подтверждением пароля, чтобы проверить их совпадение. Если будут не совпадать, то манипулятор вернется со словариком ошибок, и сообщение будет на выбранном в системе языке.

Другими словами, манипулятор — это очень хороший паттерн, который выносит сходные детали обработки любых форм (предзаполнение значениями, проверка и вывод ошибок, фиксация изменений) из кода view в отдельное место. И вьюхи становятся более читаемыми, потому что в них остается только код, говорящий "что делать", а не "как делать".

Комментарии: 9

  1. Fox DarkBlood

    Отличная статья!
    Недавно начал писать на Питоне с использованием этого фреймворка, и у мен возник такой вопрос (возможно вы можете мне в этом помочь). Суть проблемы заключается в том что я хочу сделать список друзей для каждого юзера, и использовати этот список в разных applications в проекте. Как вы считаете, как лучше это сделать? Возможно стоит создать отдельный app, а потом пользовотся его таблицами....

  2. Иван Сагалаев

    Для общих вопросов по Django у меня есть форум, ответил там.

  3. Julik

    Занятная штука эти манипуляторы. Как раз мостик между моделью и контроллером-view который можно реюзать, Надо что-то подобное для рельсов сообразить, другое дело что из всех предназначений манипулятора для рельсов имеет значение только одно

    You'll have to separately create a form (and view) that submits to this page, which is a pain and is redundant.

  4. dimas

    Отличная статья!
    Вчера даже выборочно переводил другим на #django :)
    Есть вопрос, спрошу на форуме.

  5. Давид Мзареулян

    Читаю онлайн-документацию по Джанго, и почти на каждой страничке всплывает Google Web Comments со ссылкой на Ваш блог:) Спасибо!

  6. Иван Сагалаев

    Спасибо за интересный extension :-). Правда, чтобы добиться ссылок на свой блог, пришлось переключить язык Ubuntu на русский, иначе все англоязычными забивается.

  7. Константин

    По воводу подмены контролов: а как сделать, если хочется один контрол заменить на два? Т.е., например, поле типа Float вводить двумя полями: в первом знак числа селектом, во втором модуль числа. И как его потом из этих двух полей собирать перед сохранением в базу?

  8. Иван Сагалаев

    Примерно так:
    - убрать из автоматического манипулятора стандартное поле, включить два своих
    - переопределить flatten_data, где из объекта взять значение, разделить как надо и положить в результирующий dict под именами полей
    - переопределить save, где собрать данные из двух полей в одно и записать в объект

  9. Константин

    И, наконец, вопрос, который меня мучает с самого начала изучения джанги: зачем манипуляторов два? (add и change).

Добавить комментарий