Формсеты

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.

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

Параметр 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 данных.

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

Аргумент error_messages позволяет вам переопределить сообщения по умолчанию, которые будет выдавать набор форм. Передайте словарь с ключами, соответствующими сообщениям об ошибках, которые вы хотите переопределить. Ключи сообщений об ошибках включают 'too_few_forms', 'too_many_forms' и 'missing_management_form'. Сообщения об ошибках 'too_few_forms' и 'too_many_forms' могут содержать %(num)d, которые будут заменены на min_num и max_num соответственно.

Например, вот стандартное сообщение об ошибке, когда отсутствует форма управления:

>>> 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.']
Changed in Django 4.1:

Были добавлены клавиши 'too_few_forms' и 'too_many_forms'.

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

Набор форм имеет метод 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 = set()
...         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.add(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>

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

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 был превышен из-за чрезмерного количества предоставленных начальных данных.

Сообщение об ошибке можно настроить, передав сообщение 'too_many_forms' в аргументе error_messages.

Примечание

Независимо от 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.']

Сообщение об ошибке можно настроить, передав сообщение 'too_few_forms' в аргументе error_messages.

Примечание

Независимо от 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

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

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

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. Метод принимает единственный аргумент - индекс формы в наборе форм. Для empty_form индекс является None:

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

>>> ArticleFormSet = formset_factory(MyArticleForm, formset=BaseArticleFormSet)
>>> formset = ArticleFormSet()

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

В отображаемом 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

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

BaseFormSet.template_name

Имя шаблона, отображаемого при преобразовании набора форм в строку, например, через print(formset) или в шаблоне через {{ formset }}.

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

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

Changed in Django 4.1:

В старых версиях template_name по умолчанию использовалось строковое значение 'django/forms/formset/default.html'.

BaseFormSet.template_name_div
New in Django 4.1.

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

BaseFormSet.template_name_p

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

BaseFormSet.template_name_table

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

BaseFormSet.template_name_ul

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

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

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

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

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

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

BaseFormSet.as_div()
New in Django 4.1.

Верстает набор форм с шаблоном template_name_div.

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.

Ручная визуализация 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.

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