Spec-Zone.ru › Django 4.2

Наборы форм

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 предоставляет несколько способов проверки минимального или максимального количества отправленных форм. Приложения, которым требуется более настраиваемая проверка количества форм, должны использовать пользовательскую валидацию набора форм.

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 удалит возможность удаления дополнительных форм.

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

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

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

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

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

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

BaseFormSet.renderer

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

BaseFormSet.template_name

Имя шаблона, отображаемого, если формсет приводится к строке, например, с помощью 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

Имя шаблона, используемого при вызове 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(). Все аргументы необязательны и будут иметь значения по умолчанию:

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

Вручную отображаемые 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.

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

Spec-Zone.ru

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