Наборы форм

class django.forms.formsets.BaseFormSet

Набор форм — это абстрактный слой для работы с множеством форм на одной странице. Его можно сравнить с таблицей данных. Скажем у вас есть следующая форма:

>>> 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)

Итерация по набору форм отобразит формы в порядке их определения. Вы можете изменить этот порядок, предоставив собственную версию метода __iter__().

К формами внутри набора можно обращаться по индексу. При определении собственного __iter__, вами потребуется также реализовать __getitem__ для сохранения этой возможности.

Использование начальных данных с наборами форм

Возможность назначения начальных данных является одним из ключевых аспектов удобства использования наборов форм. Как было показано выше, вы можете управлять количеством пустых форм, отображаемых набором. Это означает, что вы указываете набору сколько дополнительных пустых форм следует отобразить вместе с формами, которые заполнены начальными данными. Давайте обратимся к следующему примеру:

>>> import datetime
>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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() предоставляет вам возможность ограничить максимальное количество пустых форм, отображаемых набором:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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 (по умолчанию) устанавливает достаточное ограничение на количество отображаемых форм (1000). На практике это эквивалентно отсутствию ограничения.

Если количество инициализированных форм превышает max_num, все они будут показаны в любом случае. (Дополнительных форм не будет.)

По умолчанию, max_num просто определяет количество форм без валидации. Если передать validate_max=True в formset_factory(), max_num будет учитываться при валидации. Смотрите Проверка количества форм.

Changed in Django 1.6:

The validate_max parameter was added to formset_factory(). Also, the behavior of FormSet was brought in line with that of ModelFormSet so that it displays initial data regardless of max_num.

Проверка набора форм

Проверка набора форм практически совпадает с аналогичной задачей для обычных форм. Набор форм обладает методом is_valid(), который предоставляет удобный способ выполнить проверку всех форм набора:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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 является списком значений, которые связаны с формами набора. Проверка была выполнена для обеих форм и в результате было отображено сообщение об ошибке во второй форме.

total_error_count(self)
New in Django 1.6.

Чтобы определить общее количество ошибок, используйте метод total_error_count:

>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': [u'This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1

У нас есть возможность проверить изменились ли данные относительно начальных значений (т.е форма была отправлена пустой):

>>> 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

Назначение ManagementForm

Вы могли обратить внимание на дополнительные поля (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, вам следует увеличить счётчики в соответствующих полях этой формы. С другой стороны, если вы позволяете удалять существующие объекты через JavaScript, то вы должны обеспечить правильную маркировку этих объектов с помощью form-#-DELETE в данных POST. Ожидается, что все формы присутствуют в данных POST, даже если они “удалены”.

Доступ к форме управления возможен через атрибут набора форм. При отображении формы в шаблоне, вы можете включать в страницу все управляющие данные с помощью {{ my_formset.management_form }} (подставьте имя своего набора).

Методы total_form_count и initial_form_count

Класс BaseFormSet имеет ряд методов, которые предназначены для работы с ManagementForm: total_form_count и initial_form_count.

Метод total_form_count возвращает количество форм в наборе. Метод initial_form_count возвращает количество предварительно заполненных форм в наборе, а также используется для определения количества форм, обязательных для заполнения. Скорее всего у вас никогда не возникнет необходимости переопределения этих методов, просто запомните их назначение.

empty_form

Класс BaseFormSet имеет атрибут empty_form, который возвращает экземпляр формы с префиксом __prefix__, что может упростить динамическое создание форм с помощью JavaScript.

Собственная проверка набора форм

Набор форм имеет метод clean, аналогичный методу класса Form. Переопределите этот метод для реализации собственных проверок данных на уровне набора форм:

>>> from django.forms.formsets import BaseFormSet
>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm

>>> 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 form in self.forms:
...             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() набора форм.

Проверка количества форм

Если передать validate_max=True в formset_factory(), при валидации будет проверенно, что общее количество форм, без учета помеченных для удаления, не больше чем max_num.

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=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 2',
...     'form-1-pub_date': u'1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
[u'Please submit 1 or fewer forms.']

При validate_max=True будет ошибка даже если количество инициализированных форм превышает max_num

Если вам нужна более сложная валидация, используйте собственную валидацию для набора форм.

Примечание

Независимо от validate_max, если количество форм превышает max_num на более чем 1000, будет ошибка валидация как при включенном validate_max, и только первые 1000 форм после max_num буду провалидированны. Остальные будут проигнорированы. Это сделано для защит от атаки на память(memory exhaustion attack) через модифицированный POST запрос.

Changed in Django 1.6:

The validate_max parameter was added to formset_factory().

Сортировка и удаление форм

formset_factory() принимает два необязательных аргумента: can_order и can_delete, которые добавляют возможность сортировки и удаления форм.

can_order

Значение по умолчанию: False

Позволяет создавать наборы форм с возможностью сортировки:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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="number" 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="number" 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="number" 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'}

can_delete

Значение по умолчанию: False

Позволяет создавать наборы форм с возможностью удаления их элементов:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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'}]

Если вы используете ModelFormSet, то экземпляры моделей для удалённых форм будут уничтожены при вызове formset.save(). С другой стороны, если вы используете простой FormSet, то вам потребуется явно обрабатывать formset.deleted_forms, например в методе save() набора форм, т.к. нет общих рекомендаций по удалению форм.

Добавление дополнительных полей к набору форм

При необходимости добавить дополнительные поля к набору форм, следует воспользоваться методом add_fields, который определён в базовом классе набора форм. Вы можете просто переопределить этот метод для того, чтобы добавлять свои собственные поля или даже изменять стандартные поля и/или атрибуты сортировки и удаления полей:

>>> from django.forms.formsets import BaseFormSet
>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> 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>

Использование наборов форм в представлениях и шаблонах

Использовать набор форм внутри представления просто, как работать с экземпляром обычной формы. Единственное о чём следует помнить — об использовании управляющей формы в шаблоне. Давайте посмотрим на пример:

from django.forms.formsets import formset_factory
from django.shortcuts import render_to_response
from myapp.forms import ArticleForm

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 и can_order

Если вы вручную создаёте эти поля в шаблоне, то вы можете отобразить параметр 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 }}.

Использование нескольких наборов форм в представлении

Если это требуется, то вы можете использовать несколько наборов форм в представлении. Наборы форм очень похожи на обычные формы. Следовательно, вы можете использовать для них префиксы. Давайте рассмотрим, как можно это сделать:

from django.forms.formsets import formset_factory
from django.shortcuts import render_to_response
from myapp.forms import ArticleForm, BookForm

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,
    })

После этого вы можете отображать наборы форм как обычно. Важно помнить, что надо передать префикс набора форм в каждый экземпляр набора, это гарантирует их правильную работу.