После нескольких восторгов по поводу Django настало время написать и о кривостях. Одна из них — то, как обрабатываются файлы, закачиваемые из браузера через <input type="file">
. Вопросы, с ними связанные, с завидным постоянством появляются в джанговской рассылке.
Автоматизированная обработка форм
Автоматизация обработки форм — одна из самых сильных сторон Django. Она делается с помощью так называемых "манипуляторов", которые делают две вещи:
- конвертируют данные из внутренних типов модели БД в пригодный для HTML-формочек вид
- валидируют принятые через POST данные и конвертируют их обратно во внутренние типы
Самое приятное во всей идее манипуляторов, что Django делает их для моделей автоматически. То есть если в модели заказа описана ссылка на клиента:
class Order(meta.Model):
client = meta.ForeignKey(Client)
... для поля client
будет автоматически построен <select>
с именами клиентов, а при возврате данных из формы манипулятор проверит, что поле, во-первых, заполнено, а во-вторых, заполнено существующим id клиента. Если валидация прошла успешно, то манипулятор сможет сохранить данные в базу, а если были ошибки, то покажет ту же самую форму, но уже с заполненными текущими данными, а также с ошибками для каждого отдельного поля.
В коде все выглядит довольно понятно.
Вывод формы
# создается автоматический манипулятор для объекта
manipulator = orders.ChangeManipulator(id)
# данные объекта переводятся в "формочный" вид
data = manipulator.flatten_data()
# FormWrapper умеет рисовать элементы форм с данными
form=FormWrapper(manipulator,data,{})
return render_to_response('order_template',{'form':form})
Прием POST-данных
manipulator=orders.ChangeManipulator(id)
# данные копируются из POST для дальнейших издевательств
data=request.POST.copy()
# валидация данных
errors=manipulator.get_validation_errors(data)
if errors:
# При ошибках форма и ошибки выводятся обратно
form=FormWrapper(manipulator,data,errors)
return render_to_response('order_template',{'form':form})
else:
# Если ошибок нет, данные конвертируются и сохраняются
manipulator.do_html2python(data)
manipulator.save(data)
return HttpResponseRedirect('./')
Остается только добавить, что в таких стандартных ситуациях, когда форма представляет собой один объект, даже и этот код не надо писать, достаточно просто привязать конкретный URL к конкретному объекту.
Но это работает не всегда.
Файловые поля
В Django есть два типа полей — FileField и ImageField — которые представляются в виде файлового контрола в формах. И они отличаются от других полей тем, что для них нужно производить неочевидные (и не описанные в документации) вещи. И при выводе в форму, и при обработке.
Связаны отличия с тем, что сами файлы не хранятся в БД, а складываются в файловой системе. В самой БД хранятся только названия файлов.
Дальше я все буду рассматривать на примере модели музыкального альбома, у которого есть текстовое название и картинка обложки:
class Album(meta.Model):
title = meta.CharField(maxlength=50)
cover = meta.ImageField()
Вывод
Здесь есть двое часто наступаемых грабель.
Первые простые. Не все помнят, что для того, чтобы браузер вообще передавал файлы на сервер, надо фоме прописать enctype="multipart/form-data"
. Тут Django ни при чем, просто это часто забывается.
Вторые грабли в том, как файловое поле выводится в форму. Для обычных полей достаточно написать в шаблоне:
{{ form.title }}
... и оно превратится в
<input type="text" id="id_title" class="vTextField required" name="title" size="30" value="..." maxlength="50" />
Для файлов же надо указывать два поля:
{{ form.cover }} {{ form.cover_file }}
Первое — это скрытое поле, в котором лежит текущее название файла, а второе — непосредственно файловый контрол с кнопкой "Browse...".
Причем, как видно, сама картинка не выводится. Чтобы ее вывести, надо написать:
{% if form.data.cover %}<img src="{{ album.get_cover_url }}">{% endif %}
При этом, очевидно, файл картинки должен лежать в месте, видном с веба. Иначе никакого URL к нему Django построить не сможет.
На самом деле, в том, что файловые поля выводятся сложнее, чем обычные поля, Django не "виновато". Как раз наоборот — хорошо, что фреймворк не пытается форматировать эти вещи автоматически, а дает свободу в том, как это будет расположено в форме, и надо ли включать саму картинку. Но это не повод все это не документировать! :-)
Прием
При приеме файловых полей автоматические манипуляторы не сохраняют переданный файл. То есть по manipulator.save(data)
сохранится только имя файла в поле "cover". А дальше надо работать вручную:
album=manipulator.save(data)
album.save_cover_file(
request.FILES['cover_file']['filename'],
request.FILES['cover_file']['content'])
Тут надо еще быть внимательным, что имя файла может измениться при записи на диск, потому что Django автоматически подгоняет имя так, чтобы файл не перезаписал уже существующие в той же директории. Поэтому если вам нужно изначальное имя файла, его надо хранить в отдельном поле.
Но это все цветочки. Ягодки начинаются тогда, когда это поле надо менять или удалять. Кроме того, тот код, который я привел выше, тоже нерабочий, потому что на каждое сохранение формы будет записываться новый файл, а старые будут вечно лежать мусором, никто их не удалит.
Опуская подробности моей вчерашней отладки, когда я с каждым шагом узнавал, что "ах, и это тоже ручками...", все выливается примерно в такой алгоритм:
- посмотреть в исходный объект, есть ли у него уже сохраненный файл (поле "cover" не пустое), и получить имя файла
- сохранить новые данные манипулятором
- если по данным формы видно, что файл удаляется или там есть новый файл, удалить старый файл вручную
- если в данных формы есть новый файл, сохранить его на диск
В коде это выглядит так (можно свободно пользоваться):
old_filename=manipulator.original_object.cover and manipulator.original_object.get_cover_filename()
album=manipulator.save(data)
if (not data['cover'] or request.FILES.get('cover_file','')) and old_filename:
os.remove(old_filename)
if request.FILES.get('cover_file',''):
album.save_cover_file(
request.FILES['cover_file']['filename'],
request.FILES['cover_file']['content'])
На стороне браузера тоже потребуется одно дополнение. В том варианте, который я предлагал раньше, нет никаких средств, чтобы удалить картинку. Для этого я повесил кнопку, по которой javascript'ом очищается значение у скрытого <input name="cover">
, а также удаляется сама картинка. Именно по пустому "cover" на сервере определяется факт удаления.
Итог
Я всеми руками за то, чтобы интерфейсная часть в HTML делалась вручную, это как раз то, что нужно контролировать. Но вот перезаписывание и удаление файлов вполне может и должно делаться в манипуляторах автоматически. Кто первый сделает патч? :-)
Комментарии: 12
Не факт что в манипуляторах перезаписывание и удаление файлов нужно делать автоматически, т.к. в разных случаях это нужно делать по-разному в зависимости от задачи.
Хотя, можно написать патч для самого общего случая. И, если понадобятся какие-то извраты, то уже делать ручками :)
Кстати, смотрю исходники Django и замечаю очень много подходов, которые наша команда реализовала в своём фрэймворке на PHP. Поэтому, уверен, переход на Django будет лёгок :)
Хороший граммотный фрэймворк! Чем больше узнаю о Django, тем больше он мне нравится :)
Иван, спасибо за статьи! В своём RSS-агрегаторе всегда с нетерпением жду заметок с Вашего блога. Пешите истчо! =)))
А что, Django и на PHP могет? ;) Мне казалось что этот фреймворк только Python поддерживает... а портировать исходники с PHP на Python, imho, не тривиальная задача :)
В этом направлении работают. Вот, например, недавно в django-developers проскочило: http://code.djangoproject.com/ticket/22. Правда, это к административному интерфейсу относится.
А насчет патча: я бы пока не стал этим заниматься. Насколько я понял в magic-removal ветке от манипуляторов во многом откажутся. Во всяком случае валидацию они уже точно делать не будут. Выйдет 0.92 — можно будет посмотреть, как там дела.
Чем дальше, тем больше мне кажется, что следить за файлами должны не столько даже манипуляторы, сколько сама модель. Ведь то, что файл пишется на диск, а не в BLOB - это детали реализации полей meta.FileField. По идее, Django само должно следить за этими файлами при операциях над полями. И кстати, в одном месте оно так и делает: когда запись стирается, то файлы, к ней привязанные, очищаются. Вот хотелось бы, чтобы идея была развита и дальше:
Но сдается мне, была все таки какая-то причина, почему это не так. Видимо, там где-то есть не очень очевидные трудности с таким подходом...
Денис, да, Django для PHP нет. Но если подходы в коде похожи, то портировать, на самом деле, не так уж и сложно. Главное, чтобы не было в PHPшном коде завязок на всякие фичи PHP, которые обычно считаются "опасными", а значит не имеют прямых аналогов в других языках.
Судя по всему, из манипуляторов только валидация и переедет в модели (где ей и место). Но вот переводы объект-форма и POST-объект все равно будут делать манипуляторы.
Но то, что патч делать рано — это точно :-)
Иван, а какие функции Вы считаете там опасными? Ручка в руках человека тоже может быть опасной :) Или Вы делаете упор на новичков, которым до фильтрации — как до луны?
Я ни слова не сказал о том, чтобы переводить исходники c PHP на Django =))) Это просто невозможно. Да и не нужно никому.
Имелось в виду: фреймворк, который написала наша команда, чем-то напоминает Django - много похожих моментов и подходов.
is there any chance, you can/will translate your blog for those of us, who speak english?
btw: came here via django community site.
thanks
derelm, sorry, I don't plan this... I hardly find the time to write this blog in my native language let alone translating it :-).
However there are many blogs on Django in English. Why are you interested in english version of this blog? Or is it this particular entry?
it's not really about this specific entry, in fact i can only guess from the pictures/code examples that it's about manipulators/forms ...
somehow i assumed that what gets populated via django community site is supposed to be in english (because of general interest) - but that's just my faulty interpretation :).
so next time i see cyrillic chars in my feedreader, i'll just skip over to the next item - no problem there...