Spec-Zone.ru › Django 4.1

Формы набора

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

>>> 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, вам также следует увеличивать счетчики в этой форме. С другой стороны, если вы используете 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.']
Изменено в 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 = []
...         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>
Изменено в 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 было превышено, потому что объем исходных данных был избыточным.

Сообщение об ошибке можно настроить, передав сообщение '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

Новое в 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

Новое в 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

BaseFormSet.can_delete_extra

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

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

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

Если вам нужно добавить дополнительные поля в набор форм, это можно легко сделать. Базовый класс набора форм предоставляет метод 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 также может зависеть от конкретного экземпляра формы. Базовый класс набора форм предоставляет метод get_form_kwargs. Метод принимает единственный аргумент — индекс формы в наборе форм. Индекс равен 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

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

В рендеренном 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">

Это полезно, если вы хотите использовать более одного набора форм в представлении.

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

Наборы форм имеют следующие атрибуты и методы, связанные с рендерингом:

BaseFormSet.renderer
Новое в Django 4.0.

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

BaseFormSet.template_name
Новое в Django 4.0.

Имя шаблона, который отрисовывается, если набор форм преобразуется в строку, например, с помощью print(formset) или в шаблоне с помощью {{ formset }}.

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

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

Изменено в Django 4.1:

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

BaseFormSet.template_name_div
Новое в Django 4.1.

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

BaseFormSet.template_name_p
Новое в Django 4.0.

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

BaseFormSet.template_name_table
Новое в Django 4.0.

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

BaseFormSet.template_name_ul
Новое в Django 4.0.

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

BaseFormSet.get_context()
Новое в Django 4.0.

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

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

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

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

  • template_name: template_name
  • context: Значение, возвращаемое get_context()
  • renderer: Значение, возвращаемое renderer
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() для класса набора форм. Это отрисовывает набор форм, используя шаблон, указанный в атрибуте template_name. По умолчанию набор форм отрисовывается as_table, при этом доступны другие вспомогательные методы as_p и as_ul. Отрисовку набора форм можно настроить, указав атрибут template_name или, более общее, переопределив шаблон по умолчанию.

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

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

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.1/topics/forms/formsets/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API