Формсеты

class BaseFormSet[исходный код]

Набор форм - это уровень абстракции для работы с несколькими формами на одной странице. Лучше всего его можно сравнить с сеткой данных. Допустим, у вас есть следующая форма:

>>> from django import forms
>>> class ArticleForm(forms.Form):
...     title = forms.CharField()
...     pub_date = forms.DateField()

Возможно, вы захотите позволить пользователю создавать несколько статей одновременно. Чтобы создать набор форм из ArticleForm, нужно сделать следующее:

>>> from django.forms 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() определяет одну дополнительную форму; в следующем примере будет создан класс formset для отображения двух пустых форм:

>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)

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

Наборы форм также могут быть проиндексированы, что возвращает соответствующую форму. Если вы переопределите __iter__, вам нужно будет также переопределить __getitem__, чтобы иметь соответствующее поведение.

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

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

>>> import datetime
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, extra=2)
>>> formset = ArticleFormSet(initial=[
...     {'title': '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>

Теперь на экране отображается в общей сложности три формы. Одна для исходных данных, которые были переданы, и две дополнительные формы. Также обратите внимание, что в качестве исходных данных мы передаем список словарей.

Если вы используете initial для отображения набора форм, вы должны передать тот же initial при обработке отправки этого набора форм, чтобы набор форм мог определить, какие формы были изменены пользователем. Например, у вас может быть что-то вроде: ArticleFormSet(request.POST, initial=[...]).

Ограничение максимального количества форм

Параметр max_num в formset_factory() дает вам возможность ограничить количество форм, которые будет отображать набор форм:

>>> from django.forms 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. Например, если extra=2 и max_num=2 и набор форм инициализирован одним элементом initial, будет выведена форма для начального элемента и одна пустая форма.

Если количество элементов в начальных данных превышает max_num, то все формы с начальными данными будут отображены независимо от значения max_num, а дополнительные формы отображаться не будут. Например, если extra=3 и max_num=1 и набор форм инициализирован с двумя начальными элементами, будут отображены две формы с начальными данными.

Значение max_num None (по умолчанию) устанавливает высокий предел на количество отображаемых форм (1000). На практике это эквивалентно отсутствию ограничения.

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

Ограничение максимального количества инстанцированных форм

New in Django 3.2.

Параметр absolute_max к formset_factory() позволяет ограничить количество форм, которые могут быть инстанцированы при передаче данных POST. Это защищает от атак на истощение памяти с использованием поддельных запросов POST:

>>> from django.forms.formsets import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, absolute_max=1500)
>>> data = {
...     'form-TOTAL_FORMS': '1501',
...     'form-INITIAL_FORMS': '0',
... }
>>> formset = ArticleFormSet(data)
>>> len(formset.forms)
1500
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Please submit at most 1000 forms.']

Если absolute_max равно None, то по умолчанию используется значение max_num + 1000. (Если max_num равно None, то по умолчанию используется 2000).

Если absolute_max меньше, чем max_num, будет выдано предупреждение ValueError.

Валидация форм

Валидация с помощью набора форм почти идентична обычной Form. В наборе форм есть метод is_valid, обеспечивающий удобный способ проверки всех форм в наборе форм:

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> data = {
...     'form-TOTAL_FORMS': '1',
...     'form-INITIAL_FORMS': '0',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
True

Мы не передали никаких данных в набор форм, что привело к созданию корректной формы. Набор форм достаточно умен, чтобы игнорировать лишние формы, которые не были изменены. Если мы предоставим недопустимый артикул:

>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-0-title': 'Test',
...     'form-0-pub_date': '1904-06-16',
...     'form-1-title': 'Test',
...     'form-1-pub_date': '', # <-- this date is missing but required
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {'pub_date': ['This field is required.']}]

Как мы видим, formset.errors - это список, элементы которого соответствуют формам в наборе форм. Проверка была выполнена для каждой из двух форм, и для второго элемента появилось ожидаемое сообщение об ошибке.

Как и при использовании обычного Form, каждое поле в формах набора форм может включать HTML-атрибуты, такие как maxlength для проверки браузера. Однако поля форм наборов форм не будут включать атрибут required, поскольку такая валидация может быть некорректной при добавлении и удалении форм.

BaseFormSet.total_error_count()[исходный код]

Чтобы проверить, сколько ошибок в наборе форм, мы можем использовать метод total_error_count:

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

Можно также проверить, отличаются ли данные формы от исходных данных (т.е. форма была отправлена без каких-либо данных):

>>> data = {
...     'form-TOTAL_FORMS': '1',
...     'form-INITIAL_FORMS': '0',
...     'form-0-title': '',
...     'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.has_changed()
False

Понимание ManagementForm

Возможно, вы заметили дополнительные данные (form-TOTAL_FORMS, form-INITIAL_FORMS), которые требовались в данных набора форм выше. Эти данные требуются для ManagementForm. Эта форма используется набором форм для управления коллекцией форм, содержащихся в наборе форм. Если вы не предоставите эти данные управления, набор форм будет недействительным:

>>> data = {
...     'form-0-title': 'Test',
...     'form-0-pub_date': '',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False

Оно используется для отслеживания количества отображаемых экземпляров формы. Если вы добавляете новые формы с помощью JavaScript, вам следует увеличить поля count и в этой форме. С другой стороны, если вы используете JavaScript для удаления существующих объектов, то вам необходимо убедиться, что удаляемые объекты должным образом помечены для удаления путем включения form-#-DELETE в данные POST. Ожидается, что все формы будут присутствовать в данных POST независимо от этого.

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

Примечание

Помимо полей form-TOTAL_FORMS и form-INITIAL_FORMS, показанных в приведенных здесь примерах, форма управления также включает поля form-MIN_NUM_FORMS и form-MAX_NUM_FORMS. Они выводятся вместе с остальной частью формы управления, но только для удобства кода на стороне клиента. Эти поля не являются обязательными и поэтому не показаны в примере POST данных.

Changed in Django 3.2:

formset.is_valid() теперь возвращает False, а не вызывает исключение, когда форма управления отсутствует или была испорчена.

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.

error_messages

New in Django 3.2.

Аргумент error_messages позволяет вам переопределить сообщения по умолчанию, которые будет выдавать набор форм. Передайте словарь с ключами, соответствующими сообщениям об ошибках, которые вы хотите переопределить. Например, вот сообщение об ошибке по умолчанию, когда отсутствует форма управления:

>>> formset = ArticleFormSet({})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['ManagementForm data is missing or has been tampered with. Missing fields: form-TOTAL_FORMS, form-INITIAL_FORMS. You may need to file a bug report if the issue persists.']

А вот пользовательское сообщение об ошибке:

>>> formset = ArticleFormSet({}, error_messages={'missing_management_form': 'Sorry, something went wrong.'})
>>> formset.is_valid()
False
>>> formset.non_form_errors()
['Sorry, something went wrong.']

Пользовательская валидация набора форм

У набора форм есть метод clean, аналогичный методу класса Form. Здесь вы определяете свою собственную валидацию, которая работает на уровне набора форм:

>>> from django.core.exceptions import ValidationError
>>> from django.forms import BaseFormSet
>>> from django.forms 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:
...             if self.can_delete and self._should_delete_form(form):
...                 continue
...             title = form.cleaned_data.get('title')
...             if title in titles:
...                 raise ValidationError("Articles in a set must have distinct titles.")
...             titles.append(title)

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-0-title': 'Test',
...     'form-0-pub_date': '1904-06-16',
...     'form-1-title': 'Test',
...     'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Articles in a set must have distinct titles.']

Метод набора форм clean вызывается после вызова всех методов Form.clean. Ошибки будут найдены с помощью метода non_form_errors() на наборе форм.

Ошибки, не относящиеся к форме, будут отображаться с дополнительным классом nonform, чтобы отличить их от ошибок, специфичных для формы. Например, {{ formset.non_form_errors }} будет выглядеть следующим образом:

<ul class="errorlist nonform">
    <li>Articles in a set must have distinct titles.</li>
</ul>
Changed in Django 4.0:

Был добавлен дополнительный класс nonform.

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

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

validate_max

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

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True)
>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-0-title': 'Test',
...     'form-0-pub_date': '1904-06-16',
...     'form-1-title': 'Test 2',
...     'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at most 1 form.']

validate_max=True проверяет строго против max_num, даже если max_num был превышен из-за чрезмерного количества предоставленных начальных данных.

Примечание

Независимо от validate_max, если количество форм в наборе данных превышает absolute_max, то форма не пройдет проверку, как если бы было установлено validate_max, и дополнительно будут проверены только первые absolute_max формы. Остальные будут полностью усечены. Это сделано для защиты от атак на исчерпание памяти с использованием поддельных POST-запросов. См. Ограничение максимального количества инстанцированных форм.

validate_min

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

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, min_num=3, validate_min=True)
>>> data = {
...     'form-TOTAL_FORMS': '2',
...     'form-INITIAL_FORMS': '0',
...     'form-0-title': 'Test',
...     'form-0-pub_date': '1904-06-16',
...     'form-1-title': 'Test 2',
...     'form-1-pub_date': '1912-06-23',
... }
>>> formset = ArticleFormSet(data)
>>> formset.is_valid()
False
>>> formset.errors
[{}, {}]
>>> formset.non_form_errors()
['Please submit at least 3 forms.']

Примечание

Независимо от validate_min, если набор форм не содержит данных, то extra + min_num будут отображаться пустые формы.

Работа с заказами и удалением форм

formset_factory() предоставляет два необязательных параметра can_order и can_delete для помощи в упорядочивании форм в наборах форм и удалении форм из набора форм.

can_order

BaseFormSet.can_order

По умолчанию: False

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

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_order=True)
>>> formset = ArticleFormSet(initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': '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': '3',
...     'form-INITIAL_FORMS': '2',
...     'form-0-title': 'Article #1',
...     'form-0-pub_date': '2008-05-10',
...     'form-0-ORDER': '2',
...     'form-1-title': 'Article #2',
...     'form-1-pub_date': '2008-05-11',
...     'form-1-ORDER': '1',
...     'form-2-title': 'Article #3',
...     'form-2-pub_date': '2008-05-01',
...     'form-2-ORDER': '0',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': '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': 'Article #3'}
{'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'}
{'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'}

BaseFormSet также предоставляет атрибут ordering_widget и метод get_ordering_widget(), которые управляют виджетом, используемым с can_order.

ordering_widget

BaseFormSet.ordering_widget

По умолчанию: NumberInput

Установите ordering_widget, чтобы указать класс виджета, который будет использоваться с can_order:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     ordering_widget = HiddenInput

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True)

get_ordering_widget

BaseFormSet.get_ordering_widget()[исходный код]

Переопределите get_ordering_widget(), если вам нужно предоставить экземпляр виджета для использования с can_order:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_ordering_widget(self):
...         return HiddenInput(attrs={'class': 'ordering'})

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True)

can_delete

BaseFormSet.can_delete

По умолчанию: False

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

>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True)
>>> formset = ArticleFormSet(initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': '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-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': '3',
...     'form-INITIAL_FORMS': '2',
...     'form-0-title': 'Article #1',
...     'form-0-pub_date': '2008-05-10',
...     'form-0-DELETE': 'on',
...     'form-1-title': 'Article #2',
...     'form-1-pub_date': '2008-05-11',
...     'form-1-DELETE': '',
...     'form-2-title': '',
...     'form-2-pub_date': '',
...     'form-2-DELETE': '',
... }

>>> formset = ArticleFormSet(data, initial=[
...     {'title': 'Article #1', 'pub_date': datetime.date(2008, 5, 10)},
...     {'title': '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': 'Article #1'}]

Если вы используете ModelFormSet, экземпляры моделей для удаленных форм будут удалены при вызове formset.save().

Если вы вызовете formset.save(commit=False), объекты не будут удалены автоматически. Вам нужно будет вызвать delete() на каждом из formset.deleted_objects, чтобы действительно удалить их:

>>> instances = formset.save(commit=False)
>>> for obj in formset.deleted_objects:
...     obj.delete()

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

BaseFormSet также предоставляет атрибут deletion_widget и метод get_deletion_widget(), которые управляют виджетом, используемым с can_delete.

deletion_widget

New in Django 4.0.
BaseFormSet.deletion_widget

По умолчанию: CheckboxInput

Установите deletion_widget, чтобы указать класс виджета, который будет использоваться с can_delete:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     deletion_widget = HiddenInput

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_delete=True)

get_deletion_widget

New in Django 4.0.
BaseFormSet.get_deletion_widget()

Переопределите get_deletion_widget(), если вам нужно предоставить экземпляр виджета для использования с can_delete:

>>> from django.forms import BaseFormSet, formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def get_deletion_widget(self):
...         return HiddenInput(attrs={'class': 'deletion'})

>>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_delete=True)

can_delete_extra

New in Django 3.2.
BaseFormSet.can_delete_extra

По умолчанию: True

При установке can_delete=True, указание can_delete_extra=False удалит возможность удаления лишних форм.

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

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

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory
>>> from myapp.forms import ArticleForm
>>> class BaseArticleFormSet(BaseFormSet):
...     def add_fields(self, form, index):
...         super().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>

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

Иногда класс вашей формы принимает пользовательские параметры, например MyArticleForm. Вы можете передать этот параметр при инстанцировании набора форм:

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

>>> class MyArticleForm(ArticleForm):
...     def __init__(self, *args, user, **kwargs):
...         self.user = user
...         super().__init__(*args, **kwargs)

>>> ArticleFormSet = formset_factory(MyArticleForm)
>>> formset = ArticleFormSet(form_kwargs={'user': request.user})

Метод form_kwargs может также зависеть от конкретного экземпляра формы. Базовый класс formset предоставляет метод get_form_kwargs. Метод принимает единственный аргумент - индекс формы в наборе форм. Индекс является None для empty_form:

>>> from django.forms import BaseFormSet
>>> from django.forms import formset_factory

>>> class BaseArticleFormSet(BaseFormSet):
...     def get_form_kwargs(self, index):
...         kwargs = super().get_form_kwargs(index)
...         kwargs['custom_kwarg'] = index
...         return kwargs

Настройка префикса набора форм

В отображаемом HTML наборы форм включают префикс к имени каждого поля. По умолчанию префикс равен 'form', но его можно настроить с помощью аргумента набора форм prefix.

Например, в случае по умолчанию вы можете увидеть:

<label for="id_form-0-title">Title:</label>
<input type="text" name="form-0-title" id="id_form-0-title">

Но при ArticleFormset(prefix='article') это становится:

<label for="id_article-0-title">Title:</label>
<input type="text" name="article-0-title" id="id_article-0-title">

Это полезно, если вы хотите use more than one formset in a view.

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

Наборы форм имеют пять атрибутов и пять методов, связанных с рендерингом.

BaseFormSet.renderer
New in Django 4.0.

Определяет renderer, который будет использоваться для набора форм. По умолчанию используется рендерер, заданный параметром FORM_RENDERER.

BaseFormSet.template_name
New in Django 4.0.

Имя шаблона, используемого при вызове __str__ или render(). Этот шаблон отображает управляющую форму набора форм, а затем каждую форму в наборе форм в соответствии с шаблоном, определенным в template_name. По умолчанию это прокси as_table.

BaseFormSet.template_name_p
New in Django 4.0.

Имя шаблона, используемого при вызове as_p(). По умолчанию это 'django/forms/formsets/p.html'. Этот шаблон отображает управляющую форму набора форм, а затем каждую форму в наборе форм в соответствии с методом формы as_p().

BaseFormSet.template_name_table
New in Django 4.0.

Имя шаблона, используемого при вызове as_table(). По умолчанию это 'django/forms/formsets/table.html'. Этот шаблон отображает управляющую форму набора форм, а затем каждую форму в наборе форм в соответствии с методом формы as_table().

BaseFormSet.template_name_ul
New in Django 4.0.

Имя шаблона, используемого при вызове as_ul(). По умолчанию это 'django/forms/formsets/ul.html'. Этот шаблон отображает управляющую форму набора форм, а затем каждую форму в наборе форм в соответствии с методом формы as_ul().

BaseFormSet.get_context()
New in Django 4.0.

Возвращает контекст для отображения набора форм в шаблоне.

Доступный контекст:

  • formset : Экземпляр набора форм.
BaseFormSet.render(template_name=None, context=None, renderer=None)
New in Django 4.0.

Метод render вызывается методом __str__, а также методами as_p(), as_ul() и as_table(). Все аргументы являются необязательными и используются по умолчанию:

BaseFormSet.as_p()[исходный код]

Возвращает набор форм с шаблоном template_name_p.

BaseFormSet.as_table()[исходный код]

Возвращает набор форм с шаблоном template_name_table.

BaseFormSet.as_ul()[исходный код]

Возвращает набор форм с шаблоном template_name_ul.

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

from django.forms import formset_factory
from django.shortcuts import render
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(request, 'manage_articles.html', {'formset': formset})

Шаблон manage_articles.html может выглядеть следующим образом:

<form method="post">
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        {{ form }}
        {% endfor %}
    </table>
</form>

Однако есть небольшое сокращение, позволяющее самому набору форм работать с формой управления:

<form method="post">
    <table>
        {{ formset }}
    </table>
</form>

Вышеописанное заканчивается вызовом метода BaseFormSet.render() на классе formset. Это рендерит набор форм, используя шаблон, указанный атрибутом template_name. Как и в случае с формами, по умолчанию набор форм будет отображаться as_table, при этом доступны другие вспомогательные методы as_p и as_ul. Рендеринг набора форм можно настроить, указав атрибут template_name, или, в более общем случае, overriding the default template.

Changed in Django 4.0:

Рендеринг наборов форм был перенесен в движок шаблонов.

Ручная визуализация can_delete и can_order

Если вы вручную выводите поля в шаблоне, вы можете вывести параметр can_delete с помощью {{ form.DELETE }}:

<form method="post">
    {{ formset.management_form }}
    {% for form in formset %}
        <ul>
            <li>{{ form.title }}</li>
            <li>{{ form.pub_date }}</li>
            {% if formset.can_delete %}
                <li>{{ form.DELETE }}</li>
            {% endif %}
        </ul>
    {% endfor %}
</form>

Аналогично, если набор форм имеет возможность упорядочивания (can_order=True), то его можно визуализировать с помощью {{ form.ORDER }}.

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

При желании вы можете использовать более одного набора форм в представлении. Формсеты во многом заимствуют свое поведение у форм. Учитывая это, вы можете использовать prefix для префиксации имен полей форм набора форм с заданным значением, чтобы позволить более чем одному набору форм быть отправленным в представление без столкновения имен. Давайте посмотрим, как это можно сделать:

from django.forms import formset_factory
from django.shortcuts import render
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(request, 'manage_articles.html', {
        'article_formset': article_formset,
        'book_formset': book_formset,
    })

Затем вы отобразите наборы форм как обычно. Важно отметить, что вам нужно передать prefix как в случае POST, так и в случае не-POST, чтобы все было правильно отображено и обработано.

Каждый набор форм prefix заменяет префикс по умолчанию form, который добавляется к HTML-атрибутам каждого поля name и id.

Вернуться на верх