Формсеты¶
-
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.']
Были добавлены клавиши '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
... )
Добавление дополнительных полей в набор форм¶
Если необходимо добавить в набор форм дополнительные поля, это можно легко сделать. В базовом классе 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()
. Все аргументы являются необязательными и по умолчанию принимают значение:template_name
:template_name
context
: Значение, возвращаемоеget_context()
renderer
: Значение, возвращаемоеrenderer
-
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
.