Набор форм — это абстрактный слой для работы с множеством форм на одной странице. Его можно сравнить с таблицей данных. Скажем у вас есть следующая форма:
>>> from django import forms
>>> class ArticleForm(forms.Form):
... title = forms.CharField()
... pub_date = forms.DateField()
Вы можете позволить пользователю создавать несколько статей за один раз. Для того, чтобы создать набор форм из ArticleForm вам потребуется:
>>> from django.forms.formsets import formset_factory
>>> ArticleFormSet = formset_factory(ArticleForm)
Теперь у вас есть класс набора форм ArticleFormSet. Набор форм предоставляет возможность последовательно проходить по списку форм и отображать их как обычные формы:
>>> formset = ArticleFormSet()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
Как вы можете видеть, набор добавляет одну пустую форму к своему выводу. Количество выводимых пустых форм управляется с помощью параметра extra. По умолчанию фабрика formset_factory добавляет одну пустую форму. Следующий пример отобразит две пустые формы:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
До Django 1.3 наборы форм были неитерируемыми объектами. Для отображения набора форм приходилось использовать атрибут forms:
>>> formset = ArticleFormSet()
>>> for form in formset.forms:
... print form.as_table()
Итерация про formset.forms отобразит формы в порядке их определения. Стандартный итератор набора форм также отобразит формы в этом порядке, но вы можете изменить этот порядок, реализовав собственную версию метода __iter__().
К формами внутри набора можно обращаться по индексу. При определении собственного __iter__, вами потребуется также реализовать __getitem__ для сохранения этой возможности.
Возможность назначения начальных данных является одним из ключевых аспектов удобства использования наборов форм. Как было показано выше, вы можете управлять количеством пустых форм, отображаемых набором. Это означает, что вы указываете набору сколько дополнительных пустых форм следует отобразить вместе с формами, которые заполнены начальными данными. Давайте обратимся к следующему примеру:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Django is now open source',
... 'pub_date': datetime.date.today(),}
... ])
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Django is now open source" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-12" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
Выше показаны три формы. Одна с начальными данными и две пустые. Следует отметить, что в качестве начальных данных мы передали список словарей.
См.также
Параметр max_num фабрики formset_factory предоставляет вам возможность ограничить максимальное количество пустых форм, отображаемых набором:
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1)
>>> formset = ArticleFormset()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
Если значение параметра max_num больше количества существующих объектов, то к набору будет добавлено до extra пустых форм. Так будет происходить пока не будет достигнуто значение max_num.
Присвоение свойству max_num значения None (по умолчанию) снимает ограничения на количество отображаемых форм. Следует отметить, что стандартное значение свойства max_num было изменено в версии 1.2 с 0 на None, что позволило использовать 0 в качестве допускаемого значения.
Проверка набора форм практически совпадает с аналогичной задачей для обычных форм. Набор форм обладает методом is_valid(), который предоставляет удобный способ выполнить проверку всех форм набора:
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
... 'form-TOTAL_FORMS': u'1',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True
Мы не передали в набор форм никаких данных, но он успешно прошёл проверку. Набор обладает достаточной логикой, чтобы игнорировать дополнительные незаполненные формы. Но если мы предоставим неправильную статью:
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'1904-06-16',
... 'form-1-title': u'Test',
... 'form-1-pub_date': u'', # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': [u'This field is required.']}]
Как можно увидеть, formset.errors является списком значений, которые связаны с формами набора. Проверка была выполнена для обоих форм и в результате было отображено сообщение об ошибке во второй форме.
У нас есть возможность проверить изменились ли данные относительно начальных значений (т.е форма была отправлена пустой):
>>> data = {
... 'form-TOTAL_FORMS': u'1',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False
Вы могли обратить внимание на дополнительные поля (form-TOTAL_FORMS, form-INITIAL_FORMS и form-MAX_NUM_FORMS), которые появлялись при выводе набора форм. Эти данные необходимы для работы формы ManagementForm. Эта форма используется набором для управления своей коллекцией форм. Если эти данные не будут предоставлены набору, то будет вызвано исключение:
>>> data = {
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'',
... }
>>> formset = ArticleFormSet(data)
Traceback (most recent call last):
...
django.forms.util.ValidationError: [u'ManagementForm data is missing or has been tampered with']
Эта форма используется для хранения информации о количестве отображаемых экземпляров форм. Если вы добавляете новые формы с помощью JavaScript, то вы должны увеличивать значение поля счётчика в этой форме.
Доступ к форме управления возможен через атрибут набора форм. При отображении формы в шаблоне, вы можете включать в страницу все управляющие данные с помощью {{ my_formset.management_form }} (подставьте имя своего набора).
Класс BaseFormSet имеет ряд методов, которые предназначены для работы с ManagementForm: total_form_count и initial_form_count.
Метод total_form_count возвращает количество форм в наборе. Метод initial_form_count возвращает количество предварительно заполненных форм в наборе, а также используется для определения количества форм, обязательных для заполнения. Скорее всего у вас никогда не возникнет необходимости переопределения этих методов, просто запомните их назначение.
Класс BaseFormSet имеет атрибут empty_form, который возвращает экземпляр формы с префиксом __prefix__, что может упростить динамическое создание форм с помощью JavaScript.
Набор форм имеет метод clean, аналогичный методу класса Form. Переопределите этот метод для реализации собственных проверок данных на уровне набора форм:
>>> from django.forms.formsets import BaseFormSet
>>> class BaseArticleFormSet(BaseFormSet):
... def clean(self):
... """Checks that no two articles have the same title."""
... if any(self.errors):
... # Don't bother validating the formset unless each form is valid on its own
... return
... titles = []
... for i in range(0, self.total_form_count()):
... form = self.forms[i]
... title = form.cleaned_data['title']
... if title in titles:
... raise forms.ValidationError("Articles in a set must have distinct titles.")
... titles.append(title)
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
... 'form-TOTAL_FORMS': u'2',
... 'form-INITIAL_FORMS': u'0',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Test',
... 'form-0-pub_date': u'1904-06-16',
... 'form-1-title': u'Test',
... 'form-1-pub_date': u'1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
[u'Articles in a set must have distinct titles.']
Метод clean набора форм вызывается после вызова аналогичного метода всех форм. Ошибки будут найдены с помощью метода non_form_errors() набора форм.
Стандартными действиями над наборами форм являются сортировка и удаление экземпляров форм. Раньше вы это делали сами. Функция фабрики formset_factory принимает два необязательных аргумента can_order и can_delete, которые добавляют дополнительные поля, предоставляя простые способы доступа к данным.
Значение по умолчанию: False
Позволяет создавать наборы форм с возможностью сортировки:
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
Здесь добавляется дополнительное поле к каждой форме. Поле называется ORDER и представлено в виде forms.IntegerField. Для форм, которые были созданы с помощью начальных данных, это поле будет автоматически заполнено их порядковым номером. Давайте рассмотрим, что произойдёт, если пользователь изменит эти значения:
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-ORDER': u'2',
... 'form-1-title': u'Article #2',
... 'form-1-pub_date': u'2008-05-11',
... 'form-1-ORDER': u'1',
... 'form-2-title': u'Article #3',
... 'form-2-pub_date': u'2008-05-01',
... 'form-2-ORDER': u'0',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> formset.is_valid()
True
>>> for form in formset.ordered_forms:
... print form.cleaned_data
{'pub_date': datetime.date(2008, 5, 1), 'ORDER': 0, 'title': u'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': u'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': u'Article #1'}
Значение по умолчанию: False
Позволяет создавать наборы форм с возможностью удаления их элементов:
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> for form in formset:
.... print form.as_table()
<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" /><input type="hidden" name="form-INITIAL_FORMS" value="2" id="id_form-INITIAL_FORMS" /><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS" />
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-DELETE">Delete:</label></th><td><input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
<tr><th><label for="id_form-1-DELETE">Delete:</label></th><td><input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
<tr><th><label for="id_form-2-DELETE">Delete:</label></th><td><input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /></td></tr>
Подобно can_order, использование этого аргумента добавляет новое поле DELETE в виде forms.BooleanField. При обработке данных набора форм, вы можете получить доступ ко всем помеченным полям через свойство deleted_forms:
>>> data = {
... 'form-TOTAL_FORMS': u'3',
... 'form-INITIAL_FORMS': u'2',
... 'form-MAX_NUM_FORMS': u'',
... 'form-0-title': u'Article #1',
... 'form-0-pub_date': u'2008-05-10',
... 'form-0-DELETE': u'on',
... 'form-1-title': u'Article #2',
... 'form-1-pub_date': u'2008-05-11',
... 'form-1-DELETE': u'',
... 'form-2-title': u'',
... 'form-2-pub_date': u'',
... 'form-2-DELETE': u'',
... }
>>> formset = ArticleFormSet(data, initial=[
... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
... {'title': u'Article #2', 'pub_date': datetime.date(2008, 5, 11)},
... ])
>>> [form.cleaned_data for form in formset.deleted_forms]
[{'DELETE': True, 'pub_date': datetime.date(2008, 5, 10), 'title': u'Article #1'}]
При необходимости добавить дополнительные поля к набору форм, следует воспользоваться методом add_fields, который определён в базовом классе набора форм. Вы можете просто переопределить этот метод для того, чтобы добавлять свои собственные поля или даже изменять стандартные поля и/или атрибуты сортировки и удаления полей:
>>> class BaseArticleFormSet(BaseFormSet):
... def add_fields(self, form, index):
... super(BaseArticleFormSet, self).add_fields(form, index)
... form.fields["my_field"] = forms.CharField()
>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()
>>> for form in formset:
... print form.as_table()
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" id="id_form-0-pub_date" /></td></tr>
<tr><th><label for="id_form-0-my_field">My field:</label></th><td><input type="text" name="form-0-my_field" id="id_form-0-my_field" /></td></tr>
Использовать набор форм внутри представления также просто как работать с экземпляром обычной формы. Единственное о чём следует помнить — об использовании управляющей формы в шаблоне. Давайте посмотрим на пример:
def manage_articles(request):
ArticleFormSet = formset_factory(ArticleForm)
if request.method == 'POST':
formset = ArticleFormSet(request.POST, request.FILES)
if formset.is_valid():
# do something with the formset.cleaned_data
pass
else:
formset = ArticleFormSet()
return render_to_response('manage_articles.html', {'formset': formset})
Шаблон manage_articles.html может выглядеть так:
<form method="post" action="">
{{ formset.management_form }}
<table>
{% for form in formset %}
{{ form }}
{% endfor %}
</table>
</form>
Тем не менее, вышеприведённый код можно сократить и позволить набору самостоятельно обеспечивать вывод управляющей формы:
<form method="post" action="">
<table>
{{ formset }}
</table>
</form>
Пример завершается вызовом метода as_table набора форм.
Если вы вручную создаёте эти поля в шаблоне, то вы можете отобразить параметр can_delete с помощью {{ form.DELETE }}:
<form method="post" action="">
{{ formset.management_form }}
{% for form in formset %}
{{ form.id }}
<ul>
<li>{{ form.title }}</li>
{% if formset.can_delete %}
<li>{{ form.DELETE }}</li>
{% endif %}
</ul>
{% endfor %}
</form>
Аналогично, если набор форм может выполнять сортировку (can_order=True), то соответствующее поле можно вывести с помощью {{ form.ORDER }}.
Если это требуется, то вы можете использовать несколько наборов форм в представлении. Наборы форм очень похожи на обычные формы. Следовательно, вы можете использовать для них префиксы. Давайте рассмотрим как можно это сделать:
def manage_articles(request):
ArticleFormSet = formset_factory(ArticleForm)
BookFormSet = formset_factory(BookForm)
if request.method == 'POST':
article_formset = ArticleFormSet(request.POST, request.FILES, prefix='articles')
book_formset = BookFormSet(request.POST, request.FILES, prefix='books')
if article_formset.is_valid() and book_formset.is_valid():
# do something with the cleaned_data on the formsets.
pass
else:
article_formset = ArticleFormSet(prefix='articles')
book_formset = BookFormSet(prefix='books')
return render_to_response('manage_articles.html', {
'article_formset': article_formset,
'book_formset': book_formset,
})
После этого вы можете отображать наборы форм как обычно. Важно помнить, что надо передать префикс набора форм в каждый экземпляр набора, это гарантирует их правильную работу.
Mar 30, 2016