Вольный перевод главы 5 "Формы" из книги Marty Alchin. Pro Django. Apress, 2013, выполнен django43ru@gmail.com
Глава 5. Формы.
Введение.
Одна из ключевых составляющих современных Веб-приложений – это интерактивность, т.е. возможность получения данных от пользователя. Этими данными может быть все, что угодно, начиная от простого поискового запроса, и заканчивая романом, созданным пользователем. Очень важным является возможность обработки этих данных и превращение их в полезную информацию, представляющую ценность для всех пользователей сайта.
Процесс обмена данными сервера с пользователем начинается с отправки сервером HTML-формы в веб-браузер пользователя, где пользователь может заполнить ее и вернуть обратно на сервер. Полученные сервером данные должны быть проверены, чтобы убедиться, что, во-первых, пользователь не забыл заполнить какое-либо поле, а во-вторых, что введенные данные корректны. Если было что-то не так с введенными пользователем данными, то они должны быть отправлены пользователю обратно для исправления. Только после того, как пользователь введет все необходимые данные в форме в требуемом виде, данные могут быть использованы для дальнейшей работы с ними.
Все это можно делать без какого-либо фреймворка, однако в этом случае вам придется заниматься неинтересной, рутинной, повторяющейся работой. А это замедляет скорость разработки, провоцирует появление ошибок и усложняет их исправление.
Django решает эту проблему, обеспечивая основу для управления этими тонкостями. После того, как форма определена, Django обрабатывает детали генерации HTML-кода формы, а также обрабатывает входные данные, полученные от пользователя, производя необходимые проверки. Разумеется, при необходимости, Django позволяет обойти свой – джанговский - механизм обработки формы и дает разработчику возможность работать с формами вручную.
Объявление и определение полей формы.
Формы в Django, подобно моделям в Django, используют декларативный синтаксис для объявления полей, которые объявляются как атрибуты соответствующего класса формы. Такой подход широко используется в Django, и позволяет при минимальном синтаксисе наделить формы довольно богатым функционалом.
Но между полями модели и полями формы Django есть и отличия. Первое – это то, как класс формы и класс модели распознают свои поля. Класс модели, вообще-то говоря, не распознает тип полей модели вовсе, он просто проверяет, имеет ли поле метод contribute_to_class () и вызывает его, когда происходит обращение к этому полю. А вот формы Django проверяют тип каждого поля класса, чтобы выяснить, имеет ли поле тип django.forms.fields.Field.
Подобно моделям, формы хранят ссылки на все свои поля, однако делают это несколько иначе, нежели модели. Поля формы хранятся в двух списках – один из них называется base_fields, а другой - просто fields. В base_fields хранится список всех полей формы, которые были обнаружены при создании метакласса формы. Они принадлежат классу формы и доступны всем объектам, имеющим тип этого класса. Этот список полей подлежит изменению только в самых крайних случаях, поскольку его изменение повлечет изменение работы всех потомков класса формы. Иногда бывает полезно посмотреть на этот список полей, чтобы увидеть, какие поля принадлежат непосредственно классу формы, а какие были созданы позже (см. пример ниже).
Все экземпляры класса формы содержат, как уже было сказано выше, и второй список полей, под названием fields. В этом списке содержится перечень тех полей формы, которые будут использованы для генерации HTML-кода, а также будут проверяться на корректность ввода данных пользователем. В основном, список полей fields будет идентичен списку полей base_fields, поскольку при создании экземпляра класса формы он является точной копией своего класса-родителя. Однако иногда может потребоваться отдельная настройка формы на основе какой-либо информации, так что в общем случае разные экземпляры одного и того же класса формы могут вести себя по-разному.
Например, форма для обратной связи (ContactForm) может принимать объект типа User для того, чтобы определить, авторизован ли текущий пользователь, или нет. Если пользователь не авторизован, то в форму следует добавить поле для ввода имени пользователя:
from django import forms
class ContactForm(forms.Form):
def __init__(self, user, *args, **kwargs):
super(ContactForm, self).__init__(*args, **kwargs)
if not user.is_authenticated():
# Add a name field since the user doesn't have a name
self.fields['name'] = forms.CharField(label='Full name')
Привязка данных к форме.
Так как формы существуют для ввода данных, то привязка данных к ним должна быть осуществлена в первую очередь. Все экземпляры любых форм считаются либо «связанными», либо «несвязанными». «Связанные» формы, это формы, в поля которых введены пользовательские данные, а «несвязанные», соответственно, это те формы, в полях которых пока нет данных. Несвязанные формы, как правило, используются только для запроса необходимых данных у пользователя.
Является ли форма «связанной» или «несвязанной» определяется тем, был ли передан в нее словарь с данными, ключи которого являются именами полей этой формы, а значения ключей – соответствующие данные, введенные пользователем в поля HTML-формы. Если словарь был передан, то форма считается «связанной», и «несвязанной» - в противном случае. Этот словарь всегда передается в форму как первый позиционный аргумент, и может быть даже пустым, хотя в этом случае его польза сомнительна, поскольку такая форма вряд ли пройдет проверку на корректность введенных данных. Определить, связана форма или нет, можно проверив значения атрибута формы is_bound.
>>> from django import forms
>>> class MyForm(forms.Form):
... title = forms.CharField()
... age = forms.IntegerField()
... photo = forms.ImageField()
...
>>> MyForm().is_bound
False
>>> MyForm({'title': u'New Title', 'age': u'25'}).is_bound
True
>>> MyForm({}).is_bound
True
Обратите внимание на то, что все значения в словаре имеют тип string. Некоторые поля могут принимать значения и других типов, например, integer, но тип string является стандартным передаваем типом значения и все типы полей знают, как его обрабатывать. Это необходимо для поддержки наиболее распространенного способа создания экземпляра формы внутри какого-либо представления, с использованием словаря request.POST, доступного в представлении.
from my_app.forms import MyForm
def my_view(request):
if request.method == 'POST':
form = MyForm(request.POST)
else:
form = MyForm()
...
Иногда форма может принимать файлы, которые предоставляются иначе, нежели другие типы данных, вводимых пользователем. Файлы могут быть доступны как атрибут FILES входящего объекта запроса request. Если ваша форма имеет в себе поля типа FileField, то при ее создании вторым позиционным аргументом в конструкторе должен быть атрибут request.FILES.
from my_app.forms import MyForm
def my_view(request):
if request.method == 'POST':
form = MyForm(request.POST, request.FILES)
else:
form = MyForm()
...
Независимо от того, как был создан экземпляр формы, каждый такой экземпляр будет иметь атрибут data, который будет содержать словарь с переданными в него данными. Если форма будет «несвязанной», то этот словарь будет пустым. Нет никакой гарантии, что данные, переданные пользователем, являются корректными и безопасными, поэтому их обязательно следует проверять перед тем, как вы попытаетесь их использовать для дальнейшей работы.
Проверка введенных данных.
После того, как форма была связана с данными, данные могут быть проверены. Всегда рекомендуется проверять данные, и никогда не полагаться на пользователя. Проверка данных необходима как для предупреждения сохранения ошибочных данных, так и для предотвращения возможных атак на сайт. Проверить данные формы очень просто, для этого достаточно вызвать метод формы is_valid(), который возвращает True, если данные прошли проверку, и False – в противном случае.
def my_view(request):
if request.method == 'POST':
form = MyForm(request.POST, request.FILES)
if form.is_valid():
# Do more work here, since the data is known to be good
else:
form = MyForm()
...
НИКОГДА НЕ ДОВЕРЯЙТЕ ПОЛЬЗОВАТЕЛЮ!
Разумеется, большинство пользователей Интернета являются добропорядочными пользователями и не желают вам зла, однако встречаются и злоумышленники, которые намеренно пытаются вывести из строя ваш сайт. Во избежание проблем, просто всегда исходите из того, что данные в ваши формы вводятся ошибочные и злонамеренные, это позволит вам предотвратить многие проблемы с безопасностью. Например, пользователь не должен иметь возможность сохранять свои данные в области, предназначенные только для чтения. Он не должен иметь прямого доступа к БД и т.п. Всего можно выделить четыре основных типа атак, которые злоумышленник может предпринять, передавая некорректные данные: SQL injection, Cross-site Scripting, Cross-site Request Forgery, Form Manipulation. Для предупреждения этих атак обязательно используйте метод формы is_valid(), и никогда не полагайтесь на проверку введенных данных средствами javascript в браузере пользователя. Дело в том, что по пути от браузера до сервера у злоумышленников достаточно возможностей для внесения искажений в данные пользователя.
На самом деле, метод is_valid() делает еще больше работы, чем может показаться на первый взгляд. Во-первых, он вызывает метод формы full_clean(), который заполняет еще два атрибута формы. Первый из них – cleaned_data – словарь, аналогичный словарю, передаваемому в форму, однако содержащий в себе уже не данные вида «ключ» - «строковое значение», а «ключ» - «значение» с соответствующим python-типом. В обоих указанных словарях «ключом» выступает название поля формы. Второй – errors – словарь, который содержит перечень всех ошибок, которые возникли при попытке «очистить» входные данные и привести их к нужному python-типу. «Ключом» в словаре errors, как уже было сказано, является имя поля формы, а его значение – описание ошибки, которая возникла при «очистке» соответствующего значения. Словари error и cleaned_data «взаимодополняют» друг друга, и поле, содержащееся в одном словаре, не содержится в другом. Понятно, что в идеальном случае словарь errors должен быть пустым, а словарь full_cleaned – напротив, содержать все поля формы.
При вызове метода is_valid() происходит, среди прочего, вызов метода clean() для каждого поля формы по отдельности. Именно этот метод clean() и решает, является ли введенное значение корректным для этого поля, и в случае ввода некорректных данных возвращает описание соответствующий ошибки. В большинстве случаев такой проверки достаточно, однако иногда требуется «расширенная» проверка, и Django предоставляет возможность сделать такую проверку. Для реализации дополнительной собственной проверки корректности введенного пользователем значения для поля формы, создайте соответствующий метод в классе формы. Имя метода должно выглядеть так: cleaned_[имя поля формы](), где [имя поля формы] – ваше поле, значение которого вы и хотите проверить. Например, для поля c именем title этот метод должен называться cleaned_title(). Этот метод будет искать значение для соответствующего поля в словаре cleaned_data, проверяя его на соответствие своим требованиям. Если возникает необходимость в дополнительной «очистке» значения, то этот метод должен, после проведения соответствующей дополнительной «очистки», переписать значение поля в словарь cleaned_data.
Использование представлений, основанных на классах.
Глядя на представления, показанные до сих пор, вы наверно уже заметили, что они выглядят очень похоже друг на друга. В случае представлений, обрабатывающих данные форм, они чаще всего будут выглядеть примерно так:
from django.shortcuts import render, redirect
def my_view(request):
if request.method == 'POST':
form = MyForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('/success/')
return render(request, 'form.html', {'form': form})
else:
form = MyForm()
return render(request, 'form.html', {'form': form})
Как было показано ранее в Главе 4, это может быть сделано более контролируемым способом, путем обработки GET и POST запросов раздельно, в представлении, основанном на классе.
from django.shortcuts import render, redirect
from django.views.generic.base import View
class MyView(View):
def get(self, request):
form = MyForm()
return render(request, 'form.html', {'form': form})
def post(self, request):
form = MyForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return redirect('/success/')
return render(request, 'form.html', {'form': form})
Этот способ, безусловно, вносит некоторое улучшение в работу с формами, но он все еще требует излишних телодвижений. Почти всегда вы инициализируете и проверяете форму аналогичным образом, а также отображаете шаблоны. По-настоящему специфичным для каждой формы будет, пожалуй, лишь тот кусок кода, в котором вы что-то делаете с уже проверенной и корректно заполненной формой. В самом простом случае это может быть просто сохранение данных формы путем вызова метода form.save(), а иногда это может быть и более сложная работа, например, рассылка писем, проведение платежей и т.п.
Во избежание всей этой рутинной работы с формами, Django предоставляет представление, основанное на классе FormView. В этом представлении вы должны будете лишь указать соответствующие пути и шаблоны, а также реализовать логику метода form_valid() (и то не всегда!), который будет вызываться лишь в том случае, если переданные в форму данные пройдут все необходимые проверки.
from django.shortcuts import render, redirect
from django.views.generic.edit import FormView
class MyView(FormView):
form_class = MyForm
template_name = 'form.html'
success_url = '/success/'
def form_valid(self, form):
form.save()
return super(MyView, self).form_valid(form)
Такой подход здорово упрощает жизнь. Тем более, если учесть, что в большинстве случаев нам ничего более и не требуется, нежели сохранить полученные данные и переадресовать пользователя на success_url.
Есть несколько методов класса FormViews, которые вы можете переопределить для пущего контроля над ситуацией.
-
get_form_class(self) — Возвращает класс формы. По умолчанию, просто возвращает атрибут form_class, который имеет значение None, если вы не предоставляете что-то свое
-
get_initial(self) — Возвращает словарь, передаваемый в конструктор формы. По умолчанию – пустой словарь.
-
get_form_kwargs(self) — Возвращает словарь для использования в качестве ключевых аргументов при создании экземпляра формы для каждого запроса. По умолчанию, он включает в себя результат вызова метода get_initial(), и, если запрос был типа POST или PUT, он также добавляет request.POST и Request.Files.
-
get_form(self, form_class) — Возвращает полностью определенный экземпляр формы, имеющий класс, возвращаемый методом get_form_class(), который получает при создании аргументы, полученные из метода get_form_kwargs(). Учитывая то, что вы имеете полный контроль над всеми аргументами, передаваемыми из метода get_form_kwargs(), вызов этого метода имеет смысл использовать только для того, чтобы внести изменения в форму после того, как она была создана, но еще не была проверена.
-
form_valid(self, form) — Этот метод – основная «рабочая лошадка», который вызывается после проверки формы, если проверка прошла успешно. В нем вы можете производить собственную дополнительную проверку формы. По умолчанию, он перенаправляет пользователя по адресу, возвращаемому методом get_success_url().
-
form_invalid(self, form) — Аналог form_valid(), вот только вызывается он тогда, когда форма не прошла валидацию. По умолчанию, просто перерисовывает шаблон формы.
- get_success_url(self) — Возвращает адрес, на который должен быть перенаправлен пользователь после успешной валидации формы. По умолчанию возвращает значение атрибута success_url.
Как видите, класс FormViews позволяет настроить практически каждый аспект работы с формой. Вместо написания отдельного представления для каждого случая использования формы, вы можете настраивать только те аспекты, которые вам действительно нужны.
При работе с формой для какой-то конкретной модели используйте класс ModelForm, возможности которого еще больше. А в модуле django.views.generic.eidt есть и другие обобщенные представления, которые позволяют редактировать данные соответствующей модели:
- CreateView – используется для создания экземпляра модели.
- UpdateView – используется для редактирования существующего объекта модели.
- DeleteView – используется для удаления существующего объекта модели.
Все три представления работают схожим образом. Для работы с ними вам достаточно указать свою модель. Представления делают всю работу за вас: создают формы, проверяют введенные данные, сохраняют их в случае успешного прохождения проверки, а также перенаправляют пользователя на указанные вами urls.
from django.views.generic import edit
from my_app.models import MyModel
class CreateObject(edit.CreateView):
model = MyModel
class EditObject(edit.UpdateView):
model = MyModel
class DeleteObject(edit.DeleteView):
model = MyModel
success_url = '/'
Представление DeleteView здесь стоит несколько особняком. Для его нормальной работы, кроме модели, необходимо указать success_url – путь, по которому будет перенаправлен пользователь после успешного удаления объекта соответствующей модели. Представления CreateView и UpdateView связаны с существующим объектом, поэтому реализация по умолчанию переадресует, после обработки запроса, по адресу, возвращаемому методом get_absolute_url() соответствующего объекта. В случае же с DeleteView, удаляемый объект после удаления больше не существует, и поэтому нельзя вызвать его метод get_absolute_url().
Собственные поля.
Типы полей форм, уже существующие в Django, перекрывают потребности разработчика в большинстве случаев. Однако иногда возникает потребность в создании собственного поля формы. На самом деле, создать свое поле формы даже проще, чем создать поле модели, поскольку поле формы не требует сохранения в базу данных.
Основное отличие поля формы от поля модели заключается в том, что поле формы всегда имеет тип string, что значительно упрощает работу с ним. В этом случае не требуется обрабатывать все возможные варианты с различными типами введенных данных.
Как уже говорилось выше, все поля формы – в том числе, разумеется, и собственные поля – обязаны наследоваться от класса django.forms.fields.Field. Это позволяет форме обрабатывать свои поля должным образом. Класс поля формы определяет несколько атрибутов и методов, определяющих его поведение. Например, можно указать, какой виджет следует использовать для ввода данных в это поле, или задать описания ошибок, возникающих при «очистке» данных и т.п. Вы можете полностью определить поведение собственного поля самостоятельно.
Проверка.
Наиболее важным, пожалуй, для любого поля, является проверка корректности присвоенного ему значения – «валидация» - и его «очистка». За «проверку» и «очистку» значения отвечает метод поля clean(), который возбуждает исключение, если значение некорректное, и возвращает правильное – «очищенное» - значение в противном случае. Метод имеет простую сигнатуру вида clean(self, value), где value – значение поля, которое следует проверить, а self – просто говорит о том, что эта функция является методом соответствующего класса. В случае некорректного значения value, метод clean() должен возбудить исключение django.forms.util.ValidationError, в которое следует передать сообщение о возникшей ошибке. Если значение value оказалось подходящим, и проблем с приведением его типа не возникло, то метод clean() должен вернуть соответствующее значение поля, т.е. value, приведенное к правильному типу python.
Написание подробных и понятных сообщений об ошибках, возникающих при «очистке» данных – это достаточно важное дело. Но при этом необходимо сохранить простоту работы с сообщениями об ошибках, в то же время позволяя отдельным экземплярам переопределять их. Для упрощения этой работы, Django предоставляет два атрибута в классе Field, один из них - error_messages, и другой - default_error_messages, а также аргумент с названием error_messages. Звучит непонятно, но на самом деле работает все это довольно просто.
Класс поля формы определяет свои стандартные сообщения об ошибках в атрибуте класса, называемом default_error_messages. Это словарь, в котором к легко идентифицируемому ключу привязываются сообщения об ошибках. Так как класс поля часто наследуется от других полей, которые могут определять собственные сообщения об ошибках default_error_messages, то Django автоматически собирает все эти сообщения об ошибках при инициализации экземпляра класса поля в атрибуте error_messages.
Дополнительно к использованию default_error_messages, Django позволяет отдельным экземплярам полей переопределить некоторые из этих сообщений об ошибках, путем передачи их в качестве аргумента error_messages. Все значения в этом словаре заменят значения по умолчанию, взятые для соответствующих ключей, но только для этого конкретного экземпляра поля. Все остальные экземпляры полей того же класса останутся прежними.
Это означает, что сообщения об ошибках могут поступать из трех источников: из самого класса поля, из его родительского класса, и из аргументов, передаваемых в конструктор класса поля при его создании. При возникновении исключения, в методе clean() должен быть простой способ получения сообщения об ошибке, откуда бы оно – сообщение об ошибке – не бралось бы. Для обеспечения этой простоты, Django заполняет атрибут error_messages для каждого экземпляра поля, который содержит все сообщения об ошибках, которые были заданы всеми тремя вышеуказанными способами. Таким образом, в методе clean() просто нужно будет взять описание для возникающей ошибки из словаря self.error_measages по соответствующему ключу, и использовать это значение – описание ошибки – в качестве аргумента для ValidationError.
from django.forms import fields, util
class LatitudeField(fields.DecimalField):
default_error_messages = {'out_of_range': u'Value must be within -90 and 90.',}
def clean(self, value):
value = super(LatitudeField, self).clean(value)
if not -90 <= value <= 90:
raise util.ValidationError(self.error_messages['out_of_range'])
return value
class LongitudeField(fields.DecimalField):
default_error_messages = {'out_of_range': u'Value must be within -180 and 180.',}
def clean(self, value):
value = super(LongitudeField, self).clean(value)
if not -180 <= value <= 180:
raise util.ValidationError(self.error_messages['out_of_range'])
return value
Обратите внимание, что здесь используется метод super() для вызова метода clean() родительского класса, который проверяет, что значение value является допустимыми decimal-числом. А уже потом – если проверка на принадлежность типу decimal прошла успешно – осуществляется проверка на то, что введенное значение может быть широтой или долготой.
Работа с виджетами.
Два других атрибута, определяемых в классе поля, позволяют задать то, какие виджеты для генерации HTML-разметки поля формы будут использоваться в конкретных ситуациях. Первый атрибут – widget – определяет виджет, который будет использоваться, когда поле не определено явно. Он определен как класс виджета, а не экземпляра, и в этом случае экземпляр виджета создается одновременно с созданием экземпляра класса поля.
Второй атрибут – hidden_widget – определяет виджет, который должен быть использован в тех случаях, для поля должна быть HTML-разметка, но пользователь при этом не должен видеть ее. Не переопределяйте этот атрибут без необходимости, ибо его значение по умолчанию – HiddenInput – достаточно в большинстве случаев. Для некоторых полей, требующих указания нескольких значений, подобных, например, MultipleChoiceField, используется MultipleHiddenInput.
Дополнительно вы можете определить метод widget_attrs(self, widget_object), который позволяет добавить html-атрибуты к инициализированному экземпляру виджета. Вместо присоединения к инициализированному виджету html-атрибутов напрямую, лучше верните из метода widget_attrs() словарь нужных вам html-атрибутов. Такая техника присоединения атрибутов к виджету использована в Django, например, для добавления к полю CharField атрибута html-разметки maxlength.
Определение поведения HTML-разметки.
Виджеты определяют, как поля будут отображаться на странице пользователя. В то время как сами поля формы отвечают за проверку данных и их приведение к типу python, виджеты отвечают за ввод данных пользователем в поле на веб-странице. Django предоставляет значительный набор встроенных виджетов, от простого поля «галочка» для логического поля, до выпадающего списка, с возможностью выбора нескольких вариантов. Если вас не устраивает встроенный виджет к какому-либо полю, то вы всегда можете указать свой класс виджета, как было описано выше.
Собственные виджеты.
Подобно полям формы, виджеты для полей, предоставляемые Django, перекрывают потребности разработчиков в большинстве случаев, однако бывают случаи, когда встроенных возможностей недостаточно. Например, вы захотите показать пользователю единицы измерения вводимых значений. Иногда возникает необходимость интеграции виджета со сторонними javascript-библиотеками, которые предоставляют дополнительные возможности, например, красивый календарь для возможности выбора даты. Все эти возможности могут быть предоставлены виджетами, которые вы создадите самостоятельно, используя при этом все возможности HTML.
Желательно, чтобы ваш виджет был наследником от класса django.forms.widgets.Widget, хотя это и не обязательно. Указанный класс предоставляет базовую функциональность для виджета, и вы можете переопределить в своем виджете только нужные вам атрибуты и методы.
Генерация HTML-разметки виджета.
Основной задачей виджета является предоставление интерфейса пользователю для ввода значения. Эта задача решается путем генерации виджетом соответствующей html-разметки.
Рассмотрим пример. У нас есть поле, значение которого задано в процентах. Вы захотели к полю ввода на странице добавить значок процентов «%». Для решения этой задачи вам необходимо определить метод render(self, name, value, attrs=None) вашего виджета. В качестве аргументов в метод передаются следующие аргументы: self – говорящий о том, что эта функция является методом, name – название элемента, которое он будет иметь в html-коде, value – значение, которое должно быть отображено в поле ввода, attrs – словарь html-атрибутов для тонкой настройки вашего виджета. Вот как это реализуется в коде:
from django import forms
class PriceInput(forms.TextInput):
def render(self, name, value, attrs=None):
return '$ %s' % super(PriceInput, self).render(name, value, attrs)
class PercentInput(forms.TextInput):
def render(self, name, value, attrs=None):
return '%s %%' % super(PercentInput, self).render(name, value, attrs)
class ProductEntry(forms.Form):
sku = forms.IntegerField(label='SKU')
description = forms.CharField(widget=forms.Textarea())
price = forms.DecimalField(decimal_places=2, widget=PriceInput())
tax = forms.IntegerField(widget=PercentInput())
>>> print ProductEntry()
<tr><th><label for="id_sku">SKU:</label></th><td><input type="text"
name="sku" id="id_sku" /></td></tr>
<tr><th><label for="id_description">Description:</label></th><td>
<textarea id="id_description" rows="10" cols="40" name="description"></textarea>
</td></tr>
<tr><th><label for="id_price">Price:</label></th><td>$ <input type="text" name="price" id="id_price" /></td></tr>
<tr><th><label for="id_tax">Tax:</label></th><td><input type="text" name="tax" id="id_tax" /> %</td></tr>
Получение данных, отправленных POST-запросом.
Виджеты не только обеспечивают непосредственный ввод данных на HTML-странице, но и сопоставляют введенные данные полям формы. Их использование позволяет скрыть от полей работу с HTML-кодом, а также позволяет управлять множественным HTML –вводом данных, и заполнять поля формы на странице «значениями по умолчанию».
Метод виджета, ответственный за выполнение этой задачи – value_from_datadict(data, files, name), где:
-
data – словарь для конструктора формы, обычно это – request.POST
-
files – файлы, передаваемые конструктору формы, имеющих формат, схожий с request.FILES
-
name – имя виджета, которое формируется как имя поля + любой префикс, что был добавлен на форму
Метод value_from_datadict() использует всю эту информацию для извлечения значений, полученных от браузера, вносит необходимые изменения и возвращает значения для соответствующего поля формы для дальнейшего использования. Метод всегда должен возвращать какое-нибудь значение. Если подходящего значения от браузера не получено, то метод должен вернуть None. Все python-функции, по умолчанию, возвращают None, если не указано другого, поэтому для возврата None достаточно обеспечить отсутствие исключений внутри метода, однако считается хорошим тоном возвращать None явно.
Разделение значения поля по нескольким виджетам.
Виджеты являются связующими звеньями между полями формы и HTML-разметкой, они определяют, как поля будут отображены на странице пользователя, и как введенные пользователем значения будут отображены в поля формы. Виджеты обладают достаточными возможностями – в виде методов render() и value_from_data() – даже для того, чтобы обеспечить ввод в поле сразу из нескольких HTML-элементов, никоим образом не затрагивая при этом сами поля.
Детали реализации зависят от того, какие html-элементы вы собираетесь для этого использовать, но общая идея достаточно простая. Поле передает свое значение виджету, который решает, как отобразить полученное значение в HTML-разметке. Отображение в HTML происходит в методе виджета render(), и именно посредством этого метода виджет может «раскидать» полученное от поля значение по нескольким HTML-элементам. Например, значение с типом DateTimeField может быть «раскидано» по input-элементам для ввода даты и времени раздельно друг от друга.
Тогда, когда виджет принимает данные обратно через свой метод value_from_datadict (), он собирает все эти части вместе, в одно значение, которое затем передается обратно в поле. Таким образом, поле формы всегда имеет дело с одним значением, в то время как виджет может отображать и принимать от пользователя значение так, как вам требуется.
К несчастью, все это требуется от каждого виджета: ответственность за HTML-разметку и сборка полученных данных в одно значение. Иногда бывает полезно скомбинировать два или более уже существующих поля, используя их готовые виджеты, вместо того, чтобы реализовывать всю работу виджета своими силами. Django предоставляет для этого некоторые возможности. Если быть точным, Django предоставляет для этой цели две утилиты: поле MultiValueField, а также виджет MultiWidget, которые разработаны для совместного использования. По отдельности друг от друга они, в общем-то, не очень полезны. Вместе они обеспечивают значительные возможности, которые могут быть кастомизированы (настроены) в классах-потомках.
Если смотреть со стороны поля, то MultiValueField заботится об «очистке» данных, проверяя каждое входящее в него поле по-отдельности. MultiValueField предоставляет своим потомкам только две возможности для настройки: первое, это то, какие поля, составляющие его, должны быть объединены, и второе, это то, каким образом из полей, составляющих его, должно быть получено значение соответствующего python-типа. Например, в Django, поле типа SplitDateTimeField получается путем комбинирования полей типа DateField и TimeField, которые, взятые вместе, приводятся к значению типа dateime.
Процесс определения, какие поля должны использоваться, осуществляется в методе __init__() класса нового поля. Просто создайте кортеж, элементами которого являются экземпляры полей, которые должны быть объединены. Затем передайте этот кортеж в качестве первого аргумента методу __init__() родительского класса. Обычно все это занимает всего несколько строк кода.
«Сборка» нескольких значений полей в одно происходит в методе compress(), который, помимо self, принимает еще один аргумент – последовательность значений полей, которые должны быть скомбинированы в некое единое значение, имеющее python-тип. Внутри метода сам процесс объединения может выглядеть достаточно сложно. Здесь нужно иметь в виду несколько моментов. Во-первых, на вход метода может поступить пустая последовательность, либо последовательность, имеющая какой-либо пустой элемент, из-за того, например, что пользователь просто не ввел нужное значение. По умолчанию, все поля обязательны к заполнению, и поэтому в этом случае будет сгенерировано исключение еще до вызова метода compress(). Если же поле было объявлено как необязательное к вводу, т.е. имеет required=False, то метод compress() должен возвращать None, и никакого исключения генерироваться не должно.
Кроме того, вполне может оказаться, что будет получена лишь часть из набора значений, поскольку все эти значения вводятся через html-страницу независимо друг от друга. Опять же, если поле является обязательным для заполнения, т.е. если оно имеет required=True (значение по умолчанию), то эта ситуация обрабатывается автоматически, как было сказано выше. Но если поле является необязательным, то метод compress() должен проделать дополнительную работу для того, чтобы обеспечить получение конечного результата для этого неполного набора данных. Обычно это достигается путем последовательной проверки каждого элемента входной последовательности на соответствие каждому из элементов кортежа django.forms.fields.EMPTY_VALUE. Любая часть составного поля, оказавшаяся незаполненной, должна сгенерировать исключение, сообщающее об отсутствующем значении.
После того, как все необходимые значения были получены и проверены, метод compress() делает работу, для которой он и предназначен: собирает из набора полученных значений одно значение, имеющее необходимый python-тип. Давайте рассмотрим пример, в котором географические координаты - «широта» и «долгота» - вводятся как decimal-числа, а из них уже собирается нужный python-тип – кортеж, содержащий в себе «широту» и «долготу».
from django.forms import fields
class LatLonField(fields.MultiValueField):
def __init__(self, *args, **kwargs):
flds = (LatitudeField(), LongitudeField())
super(LatLonField, self).__init__(flds, *args, **kwargs)
def compress(self, data_list):
if data_list:
if data_list[0] in fields.EMPTY_VALUES:
raise fields.ValidationError(u'Enter a valid latitude.')
if data_list[1] in fields.EMPTY_VALUES:
raise fields.ValidationError(u'Enter a valid longitude.')
return tuple(data_list)
return None
Следующим шагом станет создание виджета, который дает возможность ввести сразу оба значения – и «широту» и «долготу». Поскольку в нашем случае значение каждого из составных полей может быть задано через обычный текстовый html-элемент ввода, то проще всего сделать свой виджет на базе MultiWidget. Поскольку MultiWidget реализует базовый функционал по визуализации данных и извлечению значений полей из запроса браузера, то нам осталось лишь реализовать преобразование сжатого значения – кортежа, состоящего из «широты» и «долготы» - в список значений для отображения соответствующих значений каждый виджетом по отдельности.
Противоположным по назначению методу compress() является метод decompress(). Он принимает в качестве аргумента исходное значение поля, имеющее необходимый python-тип, и разбивает его на составные части, возвращая последовательность значений для составляющих поля в виде кортежа или списка. Поскольку в нашем случае LatLonField уже возвращает кортеж, то нам нужно лишь предусмотреть случай, когда данные не были введены и следует вернуть кортеж из пустых данных:
from django.forms import fields, widgets
class LatLonWidget(widgets.MultiWidget):
def __init__(self, attrs=None):
wdgts = (widgets.TextInput(attrs), widgets.TextInput(attrs))
super(LatLonWidget, self).__init__(wdgts, attrs)
def decompress(self, value):
return value or (None, None)
class LatLonField(fields.MultiValueField):
widget = LatLonWidget
# Далее идет код для этого класса, приведенный чуть выше.
Настройка разметки формы.
Помимо виджетов, можно настроить и отображение форм в HTML-разметке. В отличие от предыдущих примеров, далее мы будем настраивать шаблоны, поскольку так проще настраивать разметку для отдельных форм.
Наиболее очевидная вещь, которую мы можем настроить, это – html-элемент <form>
, поскольку Джанговская форма, вообще-то говоря, не может вывести в разметку все, что нам нужно. В первую очередь так происходит потому, что не существует способа определить то, каким образом должны быть отправлены данные через эту форму – GET или POST методами, а также определить URL, который должен быть использован для этого. Указанные параметры должны настраиваться вручную для каждой формы в отдельности. Например, тег html-разметки для джанговской формы, в которой используется FileFields, должен иметь атрибут enctype=”multipart/form-data”
Помимо поведения формы при отправке данных, еще один момент является общим для всех форм – использование CSS-стилей. Есть несколько способов привязать html-элемент к CSS-стилю, но два из них используются наиболее часто: это привязка к ID элемента, и привязка к классу элемента. Оба эти идентификатора элемента – ID и класс – часто расположены в <form>
, и их несложно добавить при создании соответствующего элемента. Кроме того, часто используется настройка внешнего вида полей формы в соответствии с общим стилем сайта. Для различных «тем» сайта поля формы могут отображаться как элементы списка, либо как ячейки таблицы и т.п., Django пытается упростить процесс настройки вида формы настолько, насколько это возможно.
Вы можете выбрать, как именно форма должна отображать свои поля в шаблоне. Если укажете as_table, то поля будут обернуты в элементы <tr>
как строки таблицы <table>
, если as_ul, то поля будут обернуты в элементы <li>
HTML-списка <ul>
. Если выберите as_p, то поля будут обернуты в элементы <p>
. Ничего более добавлено в шаблон не будет, однако вы можете указать свои атрибуты для этих элементов, такие как ID и класс, например.
Указанные способы отображения полей в форме полезны, но не перекрывают всех потребностей разработчиков. В соответствие с принципом DRY, все эти способы являются лишь частными случаям использования более общего метода _html_output(), который ответственен за обертку полей формы в html-коде. Этот метод не следует прямо вызывать из формы, но он идеально подходит для собственной настройки отображения полей формы в шаблоне.
-
normal_row – HTML-код, который будет использоваться для отображения стандартной строки. Он задан в виде строки Python-формата, которая будет получать словарь. Таким образом, есть несколько значений, которые могут быть размещены здесь: ошибки (errors), метка (label), поле (field) и help_text. Из названий уже ясно, о чем идет речь, исключением того, что поле на самом деле содержит HTML-разметку, сгенерированную виджетом поля.
-
error_row – HTML-код, содержащий только сообщение об ошибке. Используется, главным образом, для отображения ошибок уровня формы, не относящиеся к определенному полю. Также он используется для показа ошибок уровня поля формы, не привязанного непосредственно к полю ввода (т.е. сообщение об ошибке ввода в поле показано где-то в другом месте, нежели в непосредственной близости к самому полю). Это также python-строка, принимающая один неименованный аргумент для отображения сообщений об ошибках. См. errors_on_separate_row далее в этом списке.
-
row_ender – разметка, используемая для определения конца строки. Вместо добавления к строкам, (а строки должны содержать собственные метки окончания строки) raw_ender используется для вставки скрытых полей к последней строке, как раз перед ее окончанием. Таким образом, всегда убедитесь, что следующее верно: normal_row.endswith (row_ender)==True
-
help_text_html – HTML-код, который будет использоваться при записи текста помощи (подсказки). Эта разметка будет располагаться непосредственно после виджета, и принимает один неименованный аргумент в формате строки, который содержит отображаемый текст.
-
errors_on_separate_row - Логическое значение, указывающее, нужно ли отображать сообщения об ошибке, содержащееся в error_row. Имейте в виду, что если сообщения полей об ошибках передаются в normal_row, а поля отображают сообщения о своих ошибках самостоятельно, то при errors_on_separate_row==True соответствующие сообщения об ошибках будут напечатаны дважды.
Доступ к отдельным полям.
Помимо возможности настройки общего вида разметки формы посредством python-кода, существует возможность настроить вид формы непосредственно в шаблоне. Таким образом, формы отлично приспособлены для максимально возможного повторного использования, и в то же время предоставляют возможности для контроля окончательного вида в каждом конкретном случае при помощи шаблонов. Поля формы являются перечислимыми объектами, поэтому внутри шаблона можно просто пройтись по ним в цикле, присваивая им необходимые атрибуты и свойства.
-
field – настоящий объект поля формы, со всеми атрибутами, ему (полю) присущими
-
data – текущее значение, связанное с полем
-
errors – список ErrorList (описан далее), содержащий все ошибки для поля
-
is_hidden – логическое поле, сообщающее о том, является ли виджет поля скрытым элементом ввода (input)
-
label_tag() – HTML-тег
<label>
и его содержимое, для использования совместно с полем -
as_widget() – отрисовка поля по умолчанию с использованием виджета этого поля
-
as_text() – поле отрисовывается как стандартное TextInput взамен отрисовки его виджетом
-
as_textarea() – поле отрисовывается как Textarea взамен отрисовки его виджетом
-
as_hidden() – поле отрисовывается как скрытый input-элемент взамен отрисовки его виджетом.
Настройка отображения ошибок.
По умолчанию, для отображения ошибок используется питоновский класс django.forms.util.ErrorList. Он ведет себя как обычный питоновский список, но имеет при этом несколько дополнительных методов для вывода своих значений в HTML. В частности, он имеет два метода – as_ul() и as_text() – которые выводят сообщения об ошибках как ненумерованные элементы списка и простой текст соответственно.
При создании собственного класса, предназначенного для отображения сообщений об ошибках, наследуйтесь от класса django.forms.util.ErrorList и перекрывайте его метод, что позволит вам отображать сообщения так, как вам нужно.
По умолчанию, в шаблонах, для отображения сообщения об ошибках, используется метод ErrorList.as_ul(), хотя можно настроить шаблоны так, чтобы они вызывали любой другой определенный вами метод. А можно прямо в шаблоне перебрать список ErrorList и каждое содержащееся в нем значение обернуть в любой нужный вам html-код.
Создав собственный класс, содержащий сообщения об ошибках, не забудьте передать его в конструктор своей формы в качестве аргумента с именем error_class.
Дополнительно, к возможности отображения сообщений об ошибках с привязкой к каждому полю в отдельности, существует и возможность вывести сообщения для всей формы сразу. Эти сообщения формируются в методе формы clean(). Для получения доступа к этим сообщениям нужно обратиться к методу формы non_field_errors().
Используемые приемы.
Формы Django предназначены прежде всего для обработки пользовательского ввода, но они могут быть использованы и более широко, позволяя усовершенствовать пользовательский интерфейс. Формы Django могут принять практически любые данные, введенные пользователем. Ниже мы рассмотрим соответствующие примеры.
Работа с частично заполненными формами: сохранение промежуточного состояния и окончательное заполнение.
Формы, как правило, предназначены для ввода нескольких данных сразу, одним приемом. Единственная причина, по которой форма может быть вновь показана пользователю, заключается в необходимости сообщить ему об ошибках, допущенных при вводе данных, чтобы пользователь мог их исправить. Если пользователю необходимо прекратить работу с формой на какое-то время, то позже, возвращаясь к ней, он вынужден вводить в нее всю информацию заново.
Хотя такой подход и соответствует общепринятому, в некоторых случаях его применение доставляет значительные неудобства пользователю, поскольку ввод данных иногда может быть непростым и утомительным занятием. В подобных случаях было бы полезно иметь возможность для сохранения частично заполненных форм, чтобы позже можно было к ним вернуться и закончить ввод данных. Реализация такого поведения выглядит не так сложно, как это может показаться на первый взгляд. Давайте рассмотрим пример, в котором пользователь вводит какую-то информацию, связанную с покупкой дома, но не делает окончательного предложения, а позже может вернуться к своей частично заполненной форме.
from django import forms
from django_localflavor_us import forms as us_forms
from pend_form.forms import PendForm
class Offer(PendForm):
name = forms.CharField(max_length=255)
phone = us_forms.USPhoneNumberField()
price = forms.IntegerField()
Обратите внимание на то, что помимо того, что в качестве родительского класса нашей формы мы берем PendForm, в остальном наша форма выглядит как стандартная форма Django.
Хранение значений с целью дальнейшего использования
Частично заполненную форму следует сохранить в базе данных. Сохраненные значения должны быть связаны с именами полей, чтобы позже можно было бы использовать их для инициализации полей формы. Это звучит подобно динамическому созданию модели, которая может быть создана автоматически, однако по некоторым причинам, такой вариант нам не подходит.
Во-первых, потому, что между полями модели и полями формы нет взаимооднозначного соответствия. Если поля модели могут однозначно определять поля формы, то поля формы не могут однозначно определять поля модели. Можно было бы, конечно, попробовать вручную реализовать такое отображение, но в этом случае это придется реализовывать для любого пользовательского поля, что в конечном итоге не практично, да и реализовать это на практике не так просто. Кроме того, хранения в полях модели подразумевает преобразование значений полей формы к соответствующим python-типам полей моделей, что потребует проверки и т.п., в то время как частично заполненные данные могут содержать ошибки. Вместо этого, мы можем опереться на тот факт, что все данные от пользователя приходят в виде строк, которые можно сохранить в поле модели типа TextField (CharField имеет ограничение в 255 символов и может быть недостаточным для наших целей). Следующим шагом следует определить, какие именно данные должны сохраняться. Очевидно, что названия полей должны быть включены в эти сохраненные данные. Кроме того, каждая форма имеет свое количество полей, и поэтому желательно для каждого поля иметь свою строку в базе данных. Ну и нужно привязать поля к конкретной форме. Проблема здесь в том, что формы не имеют какого-либо уникального идентификатора. Что и понятно, поскольку формы существуют только в момент запроса и приема введенной информации от пользователя. Однако нам нужен какой-то способ идентификации форм. Одним из приемов идентификации сложных структур является вычисление хеш-значения. Хеш-сумма не гарантирует уникальности, и поэтому требуется добавить кое-что еще, чтобы ее добиться. В случае с формами, хеш-значение можно вычислить для полного набора данных полей, так что любое изменение в названии поля и его значении приведет к изменению значения хеш-суммы. Дополнительно к хеш-сумме можно хранить путь к форме, который позволит определить, к какой именно форме относится данная хеш-сумма на случай, если имеется несколько форм с таким же набором полей. Теперь, когда мы имеем несколько частей (кусков) информации, нужно решить, как они будут связаны друг с другом. Здесь можно выделить два уровня информации: форма и значения ее полей. Каждый уровень может быть представлен отдельной моделью, а уже эти модели могут быть связаны между собой внешним ключом. Одна модель может содержать путь к форме и хеш-сумму значений ее полей, а другая модель будет содержать список полей и их значений и ссылку (внешний ключ) на первую модель, которая описывает форму.
С учетом сказанного, модуль models.py для нашего приложения pend_form мог бы выглядеть как-нибудь так:
class PendedForm(models.Model):
form_class = models.CharField(max_length=255)
hash = models.CharField(max_length=32)
class PendedValue(models.Model):
form = models.ForeignKey(PendedForm, related_name='data')
name = models.CharField(max_length=255)
value = models.TextField()
Эта простая структура способная хранить любое количество данных для любой формы. Такое решение было бы не очень эффективным с точки зрения производительности, если бы требовалось выполнять сложные запросы, но в нашем случае нам нужно лишь сохранять и считывать данные одним простым запросом.
Теперь, когда модели созданы, нам нужно обеспечить возможность их повторного использования. На самом деле, сделать это очень просто, поскольку наши модели являются обычными питоновскими классами. В дальнейшем, если вы хотите иметь формы, сохраняющие свое промежуточное состояние, то создавайте свои формы в качестве наследников от PendForm:
try:
from hashlib import md5
except:
from md5 import new as md5
from django import forms
from pend_form.models import PendedForm
class PendForm(forms.Form):
@classmethod
def get_import_path(cls):
return '%s.%s' % (cls.__module__, cls.__name__)
def hash_data(self):
content = ','.join('%s:%s' % (n, self.data[n]) for n in self.fields.keys())
return md5(content).hexdigest()
def pend(self):
import_path = self.get_import_path()
form_hash = self.hash_data()
pended_form =PendedForm.objects.get_or_create(form_class=import_path, hash=form_hash)
for name in self.fields:
pended_form.data.get_or_create(name=name, value=self.data[name])
return form_hash
Обратите внимание на «фривольное» использование метода get_or_create(). Если уже существует форма с точно такими же данными, то нет смысла сохранять такую форму дважды. Поэтому этот метод опирается на тот факт, что если существует точная копия, то она ничем не хуже оригинала.
Воссоздание частично заполненной формы.
Теперь форма может храниться в базе данных даже не будучи полностью заполненной, даже без проверки введенных значений. Но она бесполезна, если мы не имеем возможности восстановить ее. Давайте попробуем воссоздать форму из данных, которые мы сохранили.
Поскольку код для этого, по определению, должен быть выполнен до создания экземпляра формы, то может показаться, что он – этот код – должен быть уровня модуля. Однако не забывайте, что в Питоне существуют еще и методы класса. Поскольку мы хотим инкапсулировать все, что возможно, и не размазывать код по модулям, то использование метода класса в нашем случае будет хорошим решением.
Вспомним, что конструктор формы принимает словарь значений навроде request.POST, однако при реконструкции формы из сохраненных значений у нас такого словаря нет, поэтому нам придется воссоздать его вручную.
После того, как мы получили хеш-сумму, метод для восстановления формы должен объединить ее с путем импорта, извлечь значения полей из базы данных, заполнить ими словарь, создать новую копию форму с этими значениями, и вернуть форму для дальнейшего использования. Это звучит довольно устрашающе с точки зрения ожидаемого объема работ по реализации такого функционала, но в действительности все не так страшно.
На помощь нам приходит Python и один из способов создания словарей с использованием встроенной функции dict(). Эта функция может принимать различные комбинации различных аргументов, а один из самых полезных для нас вариантов – это входные аргументы в виде последовательности кортежей, каждый из которых состоит из двух элементов: «ключа» и его «значения». Поскольку QuerySets уже возвращает последовательности, да еще имеются такие инструменты как генераторы списков и генераторы выражений, позволяющие создавать новые последовательности на основе других, то наша задача заметно облегчается.
Получение пути импорта и поиск сохраненной формы несложно, и атрибут объекта data обеспечивает легкий доступ ко всем его значениям. Используя генератор выражений, значения пар «ключ»/«значение» могут быть переданы в функцию dict(), а созданный словарь передается далее в конструктор формы. Все это может быть реализовано в коде следующим образом:
@classmethod
def resume(cls, form_hash):
import_path = cls.get_import_path()
form = models.PendForm.objects.get(form_class=import_path, hash=form_hash)
data = dict((d.name, d.value) for d in form.data.all())
return cls(data)
Этот простой метод возвращает полностью сформированный экземпляр частично заполненной ранее формы. Теперь пользователь сможет закончить работу с формой, т.е. проверить введенные значения и ввести недостающие.
Весь путь (процесс) создания формы с сохранением промежуточного состояния.
Как было сказано ранее, процесс работы с формами довольно стандартен, но если мы хотим иметь возможность сохранения промежуточного состояния формы, то нам требуется кое-что пересмотреть. Процесс работы с такой формой может выглядеть как-то так:
- Показ пустой формы
- Пользователь вводит некоторые данные.
- Пользователь отправляет частично заполненную форму на сервер
- Полученные от пользователя данные проверяются.
- Если в введенных данных обнаружены ошибки, то необходимо сообщить о них пользователю
- Пользователь нажимает «Отложить»
- Введенные пользователем данные сохраняются в базе данных.
- Данные, полученные из базы данных, проверяются.
- Если в данных обнаружены ошибки, то о них нужно сообщить пользователю.
- Закончить процесс заполнения формы.
Весь процесс работы с такой формой будет выглядеть еще сложнее. Дело в том, что есть 4 варианта развития событий, в зависимости от текущего состояния. И не забывайте, что речь идет лишь об обработке данных формы, без всякого учета бизнес-логики вашего приложения.
- Пользователь запрашивает форму безо всяких данных
- Пользователь сохраняет данные, используя кнопку «Отложить»
- Пользователь запрашивает сохраненное промежуточное состояние формы
- Пользователь сохраняет данные кнопкой «Сохранить»
При этом такие действия, как проверка данных и принятие мер, специфичных для конкретного приложения, все еще необходимы. Представление, работающее с формой, принимает примерно такой вид:
from django import http
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from properties import models, forms
def make_offer(request, id, template_name='', form_hash=None):
if request.method == 'POST':
form = forms.Offer(request.POST)
if 'pend' in request.POST:
form_hash = form.pend()
return http.HttpRedirect(form_hash)
else:
if form.is_valid():
# This is where actual processing would take place
else:
if form_hash:
form = forms.Offer.resume(form_hash)
else:
form = forms.Offer()
return render_to_response(template_name, {'form': form}, context_instance=RequestContext(request))
Здесь много чего происходит, но все это имеет мало отношение к реальной покупке дома. Большая часть этого кода существует исключительно для управления различными состояниями формы, и при использовании нашей PendForm в качестве родительской формы, в дальнейшем все это придется проделывать снова и снова, что не очень-то эффективно.
Делаем это обобщенным.
Из кода представления легко увидеть, какой код повторяется, но решить, как именно избавиться от повторения, несколько сложнее. Проблема в том, что повторяется именно код, а не числовые или строковые значения.
Ранее мы видели, как обобщенные представления (generic views) позволяют избавиться от повторов в коде, позволяя специфицировать поведение в URL-паттерне. Этот подход хорошо работает с базовыми типами навроде чисел и строк, последовательностей и словарей, но с кодом все обстоит несколько иначе. Вместо того чтобы просто указывать значение, «встроенное» в URL-паттерн, этот код следует определять в отдельной функции, которую затем и следует передавать в шаблон.
Хотя такое и возможно реализовать, но это делает URL-конфигурацию более громоздкой, учитывая, что там могут быть функции более высокого порядка, объявленные над каждым блоком URL-паттернов. Лямбда-функции могли бы здесь нам помочь, но они, к сожалению, ограничены простыми выражениями, и не могут содержать в себе циклы или условия.
Одна из альтернатив – декоратор, который может быть применен к обычной функции, обеспечивая всю необходимую функциональность. В этом случае, любая python-функция может быть использована для хранения кода, который будет работать с формами. Функция, обернутая декоратором, будет выполнять только уникальный, специфичный код, а декоратор возьмет на себя рутинную, повторяющуюся работу по работе с формами. Если бы использовали декоратор, по предыдущий код мог бы выглядеть как-нибудь так:
from pend_forms.decorators import pend_form
@pend_form
def make_offer(request, id, form):
# This is where actual processing would take place
Теперь нам только осталось реализовать декоратор, который бы взял на себя выполнение всей необходимой работы. Давайте поместим его в модуль decorators.py
from django import http
from django.shortcuts import render_to_response
from django.template.context import RequestContext
from django.utils.functional import wraps
def pend_form(view):
@wraps(view)
def wrapper(request, form_class, template_name, form_hash=None, *args, **kwargs):
if request.method == 'POST':
form = form_class(request.POST)
if 'pend' in request.POST:
form_hash = form.pend()
return http.HttpRedirect(form_hash)
else:
if form.is_valid():
return view(request, form=form, *args, **kwargs)
else:
if form_hash:
form = form_class.resume(form_hash)
else:
form = form_class()
return render_to_response(template_name, {'form': form}, context_instance=RequestContext(request))
return wrapper
Теперь все что нам осталось сделать, это настройка URL-конфигурации, что обеспечивает настройку класса формы и имя шаблона. Этот декоратор будет выполнять остальное, вызывая представления только после того, как форма будет заполнена и отправлена пользователем.
Подход, основанный на классах.
Выше мы показали, как можно решить нашу проблему традиционным путем, с использованием представлений, основанных на функциях. Но проблему можно решить и иначе, опираясь на классы, аналогично тому, как в Django используются представления, основанные на классах. Ранее мы уже видел, что класс FormView предоставляет значительную функциональность для работы с формами, и мы можем попробовать расширить этот класс для получения необходимой нам функциональности. В самом деле, мы можем добавить новые метода в класс представления, поэтому нет больше нужды создавать пользовательский класс-наследник от класса Form. Это может быть сделано с использованием формы, уже имеющейся в вашем коде. Давайте начнем с получения частично заполненной формы. Некоторые методы, ранее помещенные в класс-наследник от класса Form, могут быть использованы без изменений, но мы так же должны иметь возможность передачи существующих значений в новую форму, для чего идеально подходит метод get_from_kwargs().
from django.views.generic.edit import FormView
from pend_form.models import PendedValue
class PendFormView(FormView):
form_hash_name = 'form_hash'
def get_form_kwargs(self):
"""
Returns a dictionary of arguments to pass into the form instantiation.
If resuming a pended form, this will retrieve data from the database.
"""
form_hash = self.kwargs.get(self.form_hash_name)
if form_hash:
import_path = self.get_import_path(self.get_form_class())
return {'data': self.get_pended_data(import_path, form_hash)}
else:
return super(PendFormView, self).get_form_kwargs()
# Utility methods
def get_import_path(self, form_class):
return '%s.%s' % (form_class.__module__, form_class.__name__)
def get_pended_data(self, import_path, form_hash):
data = PendedValue.objects.filter(import_path=import_path, form_hash=form_hash)
return dict((d.name, d.value) for d in data)
Поскольку цель использования нами get_form_kwargs() заключается в том, чтобы передать значения при создании формы, то все, что нам необходимо сделать здесь, это получить соответствующие значения и вернуть их вместо значений по умолчанию. Этого будет достаточно, чтобы заполнить форму значениями, если хеш-сумма формы предоставляется в URL.
Заметьте, что form_hash является атрибутом уровня класса. Это позволяет пользователям этого представления указать, что форма является частично заполненной («отложенной»). Все, что вам нужно, это указать этот атрибут как атрибут класса, и Django позволит быть форме настраиваемой, заполняя ее определенными вами значениями в качестве значений по умолчанию.
Следующим шагом мы позволим пользователям сохранять частично заполненную форму. Как мы уже определились ранее, в базу данных мы будем сохранять введенные значения и соответствующую хеш-сумму, необходимую для однозначной идентификации формы. Основную работу по сохранению указанных значений мы выполним в методе post(), поскольку эта именно то место, где обрабатывается отправленная пользователем форма.
Функционал сохранения формы включает в себя несколько операций, которые могут быть использованы повторно. Ниже приводится код, который нам понадобится для сохранения формы «на потом», и который не помешало бы немного обсудить, прежде чем объединить все вместе.
from django.views.generic.edit import FormView
from pend_form.models import PendedForm, PendedValue
class PendFormView(FormView):
pend_button_name = 'pend'
def post(self, request, *args, **kwargs):
"""
Handles POST requests with form data. If the form was pended, it doesn't follow
the normal flow, but saves the values for later instead.
"""
if self.pend_button_name in self.request.POST:
form_class = self.get_form_class()
form = self.get_form(form_class)
self.form_pended(form)
else:
super(PendFormView, self).post(request, *args, **kwargs)
# Custom methods follow
def get_import_path(self, form_class):
return '%s.%s' % (form_class.__module__, form_class.__name__)
def get_form_hash(self, form):
content = ','.join('%s:%s' % (n, form.data[n]) for n in form.fields.keys())
return md5(content).hexdigest()
def form_pended(self, form):
import_path = self.get_import_path(self.get_form_class())
form_hash = self.get_form_hash(form)
pended_form = PendedForm.objects.get_or_create(form_class=import_path, hash=form_hash)
for name in form.fields.keys():
pended_form.data.get_or_create(name=name, value=form.data[name])
return form_hash
Метод post() обычной формы перенаправляет на метод form_valid(), либо на метод form_invalid(). Однако частично заполненная форма не является ни правильной, ни неправильной. Поэтому метод метод post() должен быть переопределен с учетом этого факта. Наш переопределенный метод post() учитывает возможность того, что форма может быть «отложенной», в этом случае дальнейшая работа с ней идет в методе form_pended(). Этот метод сохраняет частично заполненную форму, вычисляет ее хеш-сумму и т.п., т.е. делает с ней все то, о чем мы говорили выше. Все вместе это будет выглядеть так:
from django.views.generic.edit import FormView
from pend_form.models import PendedForm, PendedValue
class PendFormView(FormView):
form_hash_name = 'form_hash'
pend_button_name = 'pend'
def get_form_kwargs(self):
"""
Returns a dictionary of arguments to pass into the form instantiation.
If resuming a pended form, this will retrieve data from the database.
"""
form_hash = self.kwargs.get(self.form_hash_name)
if form_hash:
import_path = self.get_import_path(self.get_form_class())
return {'data': self.get_pended_data(import_path, form_hash)}
else:
return super(PendFormView, self).get_form_kwargs()
def post(self, request, *args, **kwargs):
"""
Handles POST requests with form data. If the form was pended, it doesn't follow
the normal flow, but saves the values for later instead.
"""
if self.pend_button_name in self.request.POST:
form_class = self.get_form_class()
form = self.get_form(form_class)
self.form_pended(form)
else:
super(PendFormView, self).post(request, *args, **kwargs)
# Custom methods follow
def get_import_path(self, form_class):
return '{0}.{1}'.format(form_class.__module__, form_class.__name__)
def get_form_hash(self, form):
content = ','.join('{0}:{1}'.format(n, form.data[n]) for n in
form.fields.keys())
return md5(content).hexdigest()
def form_pended(self, form):
import_path = self.get_import_path(self.get_form_class())
form_hash = self.get_form_hash(form)
pended_form = PendedForm.objects.get_or_create(form_class=import_path, hash=form_hash)
for name in form.fields.keys():
pended_form.data.get_or_create(name=name, value=form.data[name])
return form_hash
def get_pended_data(self, import_path, form_hash):
data = PendedValue.objects.filter(import_path=import_path,
form_hash=form_hash)
return dict((d.name, d.value) for d in data)
Теперь вы можете пользоваться этим представлением точно так же, как и любым другим представлением, основанном на классах. Все, что вам потребуется для работы, это указать класс формы, переопределить, так как вам нужно, любое из значений по умолчанию, использованные в классе PendFormView или в FromView. Шаблоны, имя кнопки и URL-ы настраиваются путем переопределения в классе-наследнике от класса PendFormView. Единственное, о чем еще нужно помнить, так это о необходимости добавить соответствующую кнопку с именем «pend» в ваш шаблон.
Что дальше?
Чтобы быть действительно полезными, формы должны быть частью HTML-страницы. Вместо того, чтобы генерировать разметку формы в python-коде, вы можете создавать разметку в шаблонах, что было бы предпочтительнее с точки зрения дизайна.