После нескольких восторгов по поводу Django настало время написать и о кривостях. Одна из них -- то, как обрабатываются файлы, закачиваемые из браузера через <input type="file">. Вопросы, с ними связанные, с завидным постоянством появляются в джанговской рассылке.

Автоматизация обработки форм -- одна из самых сильных сторон Django. Она делается с помощью так называемых "манипуляторов", которые делают две вещи:
Самое приятное во всей идее манипуляторов, что 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})
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 автоматически подгоняет имя так, чтобы файл не перезаписал уже существующие в той же директории. Поэтому если вам нужно изначальное имя файла, его надо хранить в отдельном поле.
Но это все цветочки. Ягодки начинаются тогда, когда это поле надо менять или удалять. Кроме того, тот код, который я привел выше, тоже нерабочий, потому что на каждое сохранение формы будет записываться новый файл, а старые будут вечно лежать мусором, никто их не удалит.
Опуская подробности моей вчерашней отладки, когда я с каждым шагом узнавал, что "ах, и это тоже ручками...", все выливается примерно в такой алгоритм:
В коде это выглядит так (можно свободно пользоваться):
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 делалась вручную, это как раз то, что нужно контролировать. Но вот перезаписывание и удаление файлов вполне может и должно делаться в манипуляторах автоматически. Кто первый сделает патч? :-)
Комментарии: 11
MkDir
17.03.06 16:36
Не факт что в манипуляторах перезаписывание и удаление файлов нужно делать автоматически, т.к. в разных случаях это нужно делать по-разному в зависимости от задачи.
Хотя, можно написать патч для самого общего случая. И, если понадобятся какие-то извраты, то уже делать ручками :)
Кстати, смотрю исходники Django и замечаю очень много подходов, которые наша команда реализовала в своём фрэймворке на PHP. Поэтому, уверен, переход на Django будет лёгок :)
Хороший граммотный фрэймворк! Чем больше узнаю о Django, тем больше он мне нравится :)
Иван, спасибо за статьи! В своём RSS-агрегаторе всегда с нетерпением жду заметок с Вашего блога. Пешите истчо! =)))
Денис Зайцев
17.03.06 17:23
А что, Django и на PHP могет? ;) Мне казалось что этот фреймворк только Python поддерживает... а портировать исходники с PHP на Python, imho, не тривиальная задача :)
kropp
17.03.06 17:32
В этом направлении работают. Вот, например, недавно в django-developers проскочило: http://code.djangoproject.com/ticket/22. Правда, это к административному интерфейсу относится.
А насчет патча: я бы пока не стал этим заниматься. Насколько я понял в magic-removal ветке от манипуляторов во многом откажутся. Во всяком случае валидацию они уже точно делать не будут. Выйдет 0.92 — можно будет посмотреть, как там дела.
Иван Сагалаев
17.03.06 17:33
Чем дальше, тем больше мне кажется, что следить за файлами должны не столько даже манипуляторы, сколько сама модель. Ведь то, что файл пишется на диск, а не в BLOB - это детали реализации полей meta.FileField. По идее, Django само должно следить за этими файлами при операциях над полями. И кстати, в одном месте оно так и делает: когда запись стирается, то файлы, к ней привязанные, очищаются. Вот хотелось бы, чтобы идея была развита и дальше:
Но сдается мне, была все таки какая-то причина, почему это не так. Видимо, там где-то есть не очень очевидные трудности с таким подходом...
Денис, да, Django для PHP нет. Но если подходы в коде похожи, то портировать, на самом деле, не так уж и сложно. Главное, чтобы не было в PHPшном коде завязок на всякие фичи PHP, которые обычно считаются "опасными", а значит не имеют прямых аналогов в других языках.
Иван Сагалаев
17.03.06 17:35
Судя по всему, из манипуляторов только валидация и переедет в модели (где ей и место). Но вот переводы объект-форма и POST-объект все равно будут делать манипуляторы.
Но то, что патч делать рано -- это точно :-)
Лёхха
17.03.06 18:15
Иван, а какие функции Вы считаете там опасными? Ручка в руках человека тоже может быть опасной :) Или Вы делаете упор на новичков, которым до фильтрации — как до луны?
MkDir
17.03.06 20:50
Я ни слова не сказал о том, чтобы переводить исходники c PHP на Django =))) Это просто невозможно. Да и не нужно никому.
Имелось в виду: фреймворк, который написала наша команда, чем-то напоминает Django - много похожих моментов и подходов.
derelm
17.03.06 23:36
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
Иван Сагалаев
17.03.06 23:48
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?
derelm
18.03.06 04:36
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...
developers.org.ua
19.03.06 15:18
Загрузка файлов из форм в TurboGears
...