Один из архитектурных принципов 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))
Теперь представьте, что такой вот кошмар вроде поиска всего-навсего списка выбора прав пришлось бы делать не для одного поля, а для всех в случае полностью ручного манипулятора :-).
Надо добавить, что в списке полей не обязательно только заменять поля. Можно удалять совсем, можно добавлять свои, и они не обязательно должны быть стандартными, можно целиком писать свои контролы.
Бывает, что объект редактируется частично в одной форме, частично — в другой. В том же предыдущем примере с пользователем список его прав редактируется в интерфейсе администратора, а всевозможные личные данные редактируются в профайле пользователя им самим. В этом случае нужно, чтобы манипулятор формы обращал внимание только на "свои" поля, причем 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
Отличная статья!
Недавно начал писать на Питоне с использованием этого фреймворка, и у мен возник такой вопрос (возможно вы можете мне в этом помочь). Суть проблемы заключается в том что я хочу сделать список друзей для каждого юзера, и использовати этот список в разных applications в проекте. Как вы считаете, как лучше это сделать? Возможно стоит создать отдельный app, а потом пользовотся его таблицами....
Для общих вопросов по Django у меня есть форум, ответил там.
Занятная штука эти манипуляторы. Как раз мостик между моделью и контроллером-view который можно реюзать, Надо что-то подобное для рельсов сообразить, другое дело что из всех предназначений манипулятора для рельсов имеет значение только одно
You'll have to separately create a form (and view) that submits to this page, which is a pain and is redundant.Отличная статья!
Вчера даже выборочно переводил другим на #django :)
Есть вопрос, спрошу на форуме.
Читаю онлайн-документацию по Джанго, и почти на каждой страничке всплывает Google Web Comments со ссылкой на Ваш блог:) Спасибо!
Спасибо за интересный extension :-). Правда, чтобы добиться ссылок на свой блог, пришлось переключить язык Ubuntu на русский, иначе все англоязычными забивается.
По воводу подмены контролов: а как сделать, если хочется один контрол заменить на два? Т.е., например, поле типа Float вводить двумя полями: в первом знак числа селектом, во втором модуль числа. И как его потом из этих двух полей собирать перед сохранением в базу?
Примерно так:
- убрать из автоматического манипулятора стандартное поле, включить два своих
- переопределить flatten_data, где из объекта взять значение, разделить как надо и положить в результирующий dict под именами полей
- переопределить save, где собрать данные из двух полей в одно и записать в объект
И, наконец, вопрос, который меня мучает с самого начала изучения джанги: зачем манипуляторов два? (add и change).