Создание форм из моделей

ModelForm

class ModelForm

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

По этой причине Django предоставляет вспомогательный класс, который позволит вам создать класс Form по имеющейся модели.

Например:

>>> from django.forms import ModelForm
>>> from myapp.models import Article

# Create the form class.
>>> class ArticleForm(ModelForm):
...     class Meta:
...         model = Article
...         fields = ['pub_date', 'headline', 'content', 'reporter']

# Creating a form to add an article.
>>> form = ArticleForm()

# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)

Типы полей

Сгенерированный класс Form будет содержать поле формы для каждого поля модели в порядке указанном в атрибуте fields.

Каждому полю модели соответствует стандартное поле формы. Например, CharField поле модели будет представлено на форме как CharField, а ManyToManyField поле модели будет представлено как MultipleChoiceField. Ниже представлен полный список соответствия полей модели и формы:

Поле модели Поле формы
AutoField Не представлено на форме
BigAutoField Не представлено на форме
BigIntegerField IntegerField с атрибутом min_value равным -9223372036854775808 и атрибутом max_value равным 9223372036854775807.
BinaryField CharField, if editable is set to True on the model field, otherwise not represented in the form.
BooleanField BooleanField, or NullBooleanField if null=True.
CharField CharField с атрибутом max_length равным значению атрибута max_length модели и empty_value равен None, если null=True.
DateField DateField
DateTimeField DateTimeField
DecimalField DecimalField
DurationField DurationField
EmailField EmailField
FileField FileField
FilePathField FilePathField
FloatField FloatField
ForeignKey ModelChoiceField (смотри далее)
ImageField ImageField
IntegerField IntegerField
IPAddressField IPAddressField
GenericIPAddressField GenericIPAddressField
ManyToManyField ModelMultipleChoiceField (смотри далее)
NullBooleanField NullBooleanField
PositiveIntegerField IntegerField
PositiveSmallIntegerField IntegerField
SlugField SlugField
SmallAutoField Не представлено на форме
SmallIntegerField IntegerField
TextField CharField с widget=forms.Textarea
TimeField TimeField
URLField URLField
UUIDField UUIDField

Как вы могли ожидать, ForeignKey и ManyToManyField поля модели являются особыми случаями:

  • Поле ForeignKey модели представлено полем формы ModelChoiceField, которое является обычным ChoiceField, но с вариантами значений, полученными из QuerySet.
  • Поле ManyToManyField модели представлено полем формы ModelMultipleChoiceField, которое является обычным MultipleChoiceField`, но с вариантами значений, полученными из ``QuerySet.

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

  • Если у поля модели есть blank=True, тогда к полю формы будет добавлено required=False, иначе – required=True.
  • Значением атрибута label поля будет значение поля verbose_name модели, причём первый символ этого значения будет преобразован в верхний регистр.
  • Значением атрибута help_text поля формы будет значение атрибута help_text поля модели.
  • Если для поля модели установлен атрибут choices, тогда для поля формы будет использоваться виджет Select, который будет отображать содержимое этого атрибута. Варианты выбора обычно содержат пустой вариант, который выбран по умолчанию. Если поле является обязательным, то оно требует от пользователя сделать выбор. Пустой вариант не отображается, если у поля модели есть атрибут blank=False и явное значение default (при этом, это значение будет выбрано по умолчанию).

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

Полный пример

Рассмотрим этот набор полей:

from django.db import models
from django.forms import ModelForm

TITLE_CHOICES = [
    ('MR', 'Mr.'),
    ('MRS', 'Mrs.'),
    ('MS', 'Ms.'),
]

class Author(models.Model):
    name = models.CharField(max_length=100)
    title = models.CharField(max_length=3, choices=TITLE_CHOICES)
    birth_date = models.DateField(blank=True, null=True)

    def __str__(self):
        return self.name

class Book(models.Model):
    name = models.CharField(max_length=100)
    authors = models.ManyToManyField(Author)

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ['name', 'title', 'birth_date']

class BookForm(ModelForm):
    class Meta:
        model = Book
        fields = ['name', 'authors']

Для этих моделей показанные выше классы ModelForm будут аналогичны следующим формам (разница будет только в методе save(), что мы вскоре рассмотрим.):

from django import forms

class AuthorForm(forms.Form):
    name = forms.CharField(max_length=100)
    title = forms.CharField(
        max_length=3,
        widget=forms.Select(choices=TITLE_CHOICES),
    )
    birth_date = forms.DateField(required=False)

class BookForm(forms.Form):
    name = forms.CharField(max_length=100)
    authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all())

Валидация в ModelForm

Есть два основных шага при валидации ModelForm:

  1. Валидация форм
  2. Валидация объекта модели

Как и валидация в обычной форме валидация в модельной форме выполняется при вызове is_valid() или при обращении к атрибуту errors, или при явном вызове full_clean(), но на практике вы не будете использовать последний метод.

Валидация модели (Model.full_clean()) выполняется после валидации формы, сразу после завершения метода clean().

Предупреждение

Процесс валидации изменяет объект модели переданный в конструктор ModelForm. Например, поля даты модели преобразуют значения в объект даты. Ошибка валидации может оставить объект в неопределенном состоянии и лучше не использовать его в последующем коде.

Переопределение метода clean()

Вы можете переопределить метод clean() модели для того, чтобы обеспечить дополнительную проверку. Всё это аналогично работе с обычной формой.

Экземпляр модельной формы, привязанный к объекту модели имеет атрибут instance, через который методы модельной формы имеют доступ к соответствующему экземпляру модели.

Предупреждение

Метод ModelForm.clean() устанавливает флаг, который указывает валидации модели провалидировать уникальность полей отмеченных unique, unique_together или unique_for_date|month|year.

Если вы хотите переопределить метод clean(), вызовите метод clean() родительского класса.

Взаимодействие с механизмами модели

В процессе проверки данных ModelForm будет вызывать метод clean() каждого поля вашей модели, соответствующего полю формы. Для полей модели, которые были исключены из формы, проверка данных производиться не будет. Обратитесь к документации по проверке форм для получения информации о том, как работает проверка данных поля.

Метод модели clean() вызывается перед проверкой уникальности полей. Смотрите валидацию объектов модели, чтобы узнать как работает clean().

Определение error_messages

Сообщения ошибки из form field или form Meta имеют приоритет над сообщениями ошибок из model field.

Error messages defined on model fields are only used when the ValidationError is raised during the model validation step and no corresponding error messages are defined at the form level.

Вы можете переопределить сообщения об ошибке для NON_FIELD_ERRORS, который были вызваны при валидации модели, определив ключ NON_FIELD_ERRORS в атрибут error_messages класса ModelForm.Meta:

from django.core.exceptions import NON_FIELD_ERRORS
from django.forms import ModelForm

class ArticleForm(ModelForm):
    class Meta:
        error_messages = {
            NON_FIELD_ERRORS: {
                'unique_together': "%(model_name)s's %(field_labels)s are not unique.",
            }
        }

Метод save()

Every ModelForm also has a save() method. This method creates and saves a database object from the data bound to the form. A subclass of ModelForm can accept an existing model instance as the keyword argument instance; if this is supplied, save() will update that instance. If it’s not supplied, save() will create a new instance of the specified model:

>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm

# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)

# Save a new Article object from the form's data.
>>> new_article = f.save()

# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()

Обратите внимание, если форма не была проверена, вызов save() выполнит ее, обратившись к form.errors. Если данные не верны, будет вызвано исключение ValueError – то есть, если form.errors равно True.

Если данные формы не содержат значение необязательного поля, полученный экземпляр модели будет использовать значение default, если такое указанно для поля. Такое поведение не используется для CheckboxInput, CheckboxSelectMultiple или SelectMultiple (или любой другой виджет, метод value_omitted_from_data() которого всегда возвращает False) т.к. не отмеченный «чекбокс» или <select multiple> не передаются при отправке HTML формы. Используйте собственные виджеты и поля формы, если вы проектируете API и хотите, чтобы использовалось значение по умолчанию для полей с этими виджетами.

Метод save() принимает необязательный именованный аргумент commit, который может иметь значения True или False. При вызове save() с commit=False метод вернёт объект, который ещё не был сохранён в базе данных. В этом случае, вам самостоятельно придётся вызвать метод save() у полученного объекта. Это бывает полезно, когда требуется выполнить дополнительные действия над объектом до его сохранения или если вам требуется воспользоваться одним из параметров сохранения модели. Атрибут commit по умолчанию имеет значение True.

Использование commit=False также полезно в случае, когда ваша модель имеет связь «многие-ко-многим» с другой моделью. Для такой модели, если метод save() вызван с аргументом commit=False, то Django не может немедленно сохранить данные для такой связи, т.к. невозможно создать связи для объекта, который не сохранен в базе данных.

To work around this problem, every time you save a form using commit=False, Django adds a save_m2m() method to your ModelForm subclass. After you’ve manually saved the instance produced by the form, you can invoke save_m2m() to save the many-to-many form data. For example:

# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)

# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)

# Modify the author in some way.
>>> new_author.some_field = 'some_value'

# Save the new instance.
>>> new_author.save()

# Now, save the many-to-many data for the form.
>>> f.save_m2m()

Calling save_m2m() is only required if you use save(commit=False). When you use a save() on a form, all data – including many-to-many data – is saved without the need for any additional method calls. For example:

# Create a form instance with POST data.
>>> a = Author()
>>> f = AuthorForm(request.POST, instance=a)

# Create and save the new author instance. There's no need to do anything else.
>>> new_author = f.save()

Если не принимать во внимание методы save() и save_m2m(), то ModelForm работает аналогично обычной Form. Например, метод is_valid() используется для проверки данных, метод is_multipart() используется для определения загрузки файла (в этом случае request.FILES должен быть передан форме) и так далее. Обратитесь к документу Привязка загруженных файлов к форме для получения подробностей.

Указываем какие поля использовать

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

Самый простой способ указать поля - автоматически добавить все или исключить определенные. Но такой способ не безопасен (например, случай на GitHub).

Но если вы уверены в том, что делаете, вот как использовать этот подход:

  1. В параметре fields указать специальное значение '__all__', которое указывает использовать все поля модели. Например:

    from django.forms import ModelForm
    
    class AuthorForm(ModelForm):
        class Meta:
            model = Author
            fields = '__all__'
    
  2. Используйте атрибут exclude внутреннего класса ModelForm.Meta. Этот атрибут, если он указан, должен содержать список имён полей, которые не должны отображаться на форме.

    Например:

    class PartialAuthorForm(ModelForm):
        class Meta:
            model = Author
            exclude = ['title']
    

    Так как модель Author содержит три поля: „name“, „title „ и „birth_date“, то форма будут отображать поля name и birth_date.

При использовании одного из этих способов, порядок полей в форме будет аналогичен порядку полей в модели, ManyToManyField поля будут в конце.

Если поле модели содержит editable=False, каждая форма, созданная по модели с помощью ModelForm, не будет включать в себя это поле.

Примечание

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

Django будет препятствовать всем попыткам сохранить неполную модель. Таким образом, если модель требует заполнения определённых полей и для них не предоставлено значение по умолчанию, то сохранить форму для такой модели не получится. Для решения этой проблемы вам потребуется создать экземпляр такой модели, передав ему начальные значения для обязательных, но незаполненных полей:

author = Author(title='Mr')
form = PartialAuthorForm(request.POST, instance=author)
form.save()

В качестве альтернативы, вы можете использовать save(commit=False) и вручную определить все необходимые поля:

form = PartialAuthorForm(request.POST)
author = form.save(commit=False)
author.title = 'Mr'
author.save()

Обратитесь к разделу section on saving forms для подробностей по использованию save(commit=False).

Переопределение стандартных типов полей или виджетов

Стандартные типы полей, описанные выше, имеют целесообразные настройки по умолчанию. Если ваша модель имеет поля типа DateField, то скорее всего вы пожелаете, чтобы форма использовала поле DateField для его отображения. Но класс ModelForm представляет широкие возможности по управлению типами полей формы, а также виджетами для их представления.

Для того, чтобы указать собственный виджет для поля, следует использовать атрибут widgets внутреннего класса Meta. Его значением должен быть словарь, ключами которого являются имена полей, а значениями — классы или экземпляры виджетов.

Например, если необходимо использовать CharField для того, чтобы поле name модели Author было представлено в виде <textarea> вместо стандартного <input type="text">, то вы можете переопределить виджет поля:

from django.forms import ModelForm, Textarea
from myapp.models import Author

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        widgets = {
            'name': Textarea(attrs={'cols': 80, 'rows': 20}),
        }

The widgets dictionary accepts either widget instances (e.g., Textarea(...)) or classes (e.g., Textarea). Note that the widgets dictionary is ignored for a model field with a non-empty choices attribute. In this case, you must override the form field to use a different widget.

Аналогично можно переопределить параметры labels, help_texts и error_messages указав в Meta.

Например, для переопределим параметры поля name:

from django.utils.translation import gettext_lazy as _

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title', 'birth_date')
        labels = {
            'name': _('Writer'),
        }
        help_texts = {
            'name': _('Some useful help text.'),
        }
        error_messages = {
            'name': {
                'max_length': _("This writer's name is too long."),
            },
        }

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

Например, если вы хотите использовать MySlugFormField для поля slug, вы можете сделать следующее:

from django.forms import ModelForm
from myapp.models import Article

class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
        field_classes = {
            'slug': MySlugFormField,
        }

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

Чтобы переопределить валидаторы поля, укажите их в аргументе validators:

from django.forms import CharField, ModelForm
from myapp.models import Article

class ArticleForm(ModelForm):
    slug = CharField(validators=[validate_slug])

    class Meta:
        model = Article
        fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']

Примечание

Когда вы явно создаете поле формы, необходимо знать как связанны ModelForm и Form.

ModelForm это дочерний класс Form, который может автоматически создавать поля формы. При создании полей учитываются параметры класса Meta и явно определенные поля формы. ModelForm автоматически создаст только те поля, которые отсутствуют в форме.

Явно определенные в классе поля создаются как есть, параметры Meta, такие как widgets, labels, help_texts или error_messages, игнорируются, они учитываются только для создании дополнительных полей.

При явном создании поля, Django предполагает, что вы будете определять поведение формы в целом. Следовательно, стандартные атрибуты модели (такие как max_length или required) не передаются полям формы. Если вам потребуется обеспечить поведение, определённое в модели, вам потребуется явно установить соответствующие аргументы при определении поля формы.

Например, если модель Article выглядит так:

class Article(models.Model):
    headline = models.CharField(
        max_length=200,
        null=True,
        blank=True,
        help_text='Use puns liberally',
    )
    content = models.TextField()

и вы желаете выполнить свою проверку поля headline, оставляя неизменными атрибуты blank и help_text, вы можете определить ArticleForm следующим образом:

class ArticleForm(ModelForm):
    headline = MyFormField(
        max_length=200,
        required=False,
        help_text='Use puns liberally',
    )

    class Meta:
        model = Article
        fields = ['headline', 'content']

Тип поля формы должен работать с типом значения соответствующего поля модели. Если они не соответствуют - вы получите ValueError.

Обратитесь к документации на поля формы для получения дополнительной информации о полях и их аргументах.

Локализация в полях

По умолчанию поля в ModelForm не локализируют свои данные. Для локализации полей можно использовать параметр localized_fields класса Meta.

>>> from django.forms import ModelForm
>>> from myapp.models import Author
>>> class AuthorForm(ModelForm):
...     class Meta:
...         model = Author
...         localized_fields = ('birth_date',)

Если в localized_fields указать '__all__', будут локализированы все поля.

Наследование форм

Аналогично обычным формам, вы можете наследоваться ModelForm. Это удобно когда надо добавить дополнительные поля или методы к базовому классу и использовать результат для создания других модельных форм. Например, для класса ArticleForm:

>>> class EnhancedArticleForm(ArticleForm):
...     def clean_pub_date(self):
...         ...

Мы создали форму, аналогичную ArticleForm, добавив дополнительную проверку и обработку для поля pub_date.

Вы также можете наследовать внутренний класс Meta, если требуется внести изменения в списки Meta.fields или Meta.exclude:

>>> class RestrictedArticleForm(EnhancedArticleForm):
...     class Meta(ArticleForm.Meta):
...         exclude = ('body',)

Здесь мы добавили метод из EnhancedArticleForm и изменили оригинальный ArticleForm.Meta, убрав одно поле.

Тем не менее, надо уточнить несколько моментов.

  • Применяются стандартные правила языка Python для разрешения имён. Если ваш класс унаследован от нескольких базовых классов, которые обладают внутренним классом Meta, и для него не определён собственный Meta класс, то этот класс будет унаследован из первого базового.

  • Можно унаследоваться одновременно от Form и ModelForm, однако, ModelForm должен быть первым в MRO. Т.к. эти классы используют разные мета-классы, а класс может использовать только один метакласс.

  • Можно декларативно удалить Field родительского класса, указав в названии None в дочернем классе.

    Таким способом можно исключить только те поля, которые были декларативно описаны в родительском классе. Поле ModelForm будет в любом случае созданы мета-классом. Чтобы переопределить их, используйте метод описанный в Указываем какие поля использовать.

Передача начальных значений

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

>>> article = Article.objects.get(pk=1)
>>> article.headline
'My headline'
>>> form = ArticleForm(initial={'headline': 'Initial headline'}, instance=article)
>>> form['headline'].value()
'Initial headline'

Функция-фабрика модельных форм

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

>>> from django.forms import modelform_factory
>>> from myapp.models import Book
>>> BookForm = modelform_factory(Book, fields=("author", "title"))

This can also be used to make modifications to existing forms, for example by specifying the widgets to be used for a given field:

>>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm,
...                          widgets={"title": Textarea()})

Указать используемые поля можно с помощью аргументов fields и exclude. Смотрите описание ModelForm Указываем какие поля использовать.

… или включить локализацию для полей:

>>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",))

Наборы модельных форм

class models.BaseModelFormSet

Like regular formsets, Django provides a couple of enhanced formset classes to make working with Django models more convenient. Let’s reuse the Author model from above:

>>> from django.forms import modelformset_factory
>>> from myapp.models import Author
>>> AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))

Использование аргумента``fields`` ограничивает набор форм указанным списком полей. В качестве альтернативы можно определить список полей, которые не должны отображаться на формах. Сделать это можно с помощью аргумента exclude:

>>> AuthorFormSet = modelformset_factory(Author, exclude=('birth_date',))

Этот код создаст набор форм, которые будут работать с данными модели Author. По функционалу набор модельных форм аналогичен набору обычных форм:

>>> formset = AuthorFormSet()
>>> print(formset)
<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS"><input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" maxlength="100"></td></tr>
<tr><th><label for="id_form-0-title">Title:</label></th><td><select name="form-0-title" id="id_form-0-title">
<option value="" selected>---------</option>
<option value="MR">Mr.</option>
<option value="MRS">Mrs.</option>
<option value="MS">Ms.</option>
</select><input type="hidden" name="form-0-id" id="id_form-0-id"></td></tr>

Примечание

modelformset_factory() uses formset_factory() to generate formsets. This means that a model formset is an extension of a basic formset that knows how to interact with a particular model.

Примечание

When using multi-table inheritance, forms generated by a formset factory will contain a parent link field (by default <parent_model_name>_ptr) instead of an id field.

Изменение выборки

По умолчанию, при создании набора модельных форм используется выборка, которая содержит все объекты модели (т.е., Author.objects.all()). Такое поведение можно скорректировать, используя аргумент queryset:

>>> formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))

Также вы можете унаследоваться от класса набора модельных форм и определить self.queryset в конструкторе, указав необходимые параметры выборки:

from django.forms import BaseModelFormSet
from myapp.models import Author

class BaseAuthorFormSet(BaseModelFormSet):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.queryset = Author.objects.filter(name__startswith='O')

Теперь передадим ваш класс BaseAuthorFormSet в функцию фабрики:

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title'), formset=BaseAuthorFormSet)

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

>>> AuthorFormSet(queryset=Author.objects.none())

Изменение формы

По умолчанию, когда вы используете modelformset_factory, форма будет создана с помощью modelform_factory(). Часто необходимо указать свою форму. Например, форму с собственной валидацией:

class AuthorForm(forms.ModelForm):
    class Meta:
        model = Author
        fields = ('name', 'title')

    def clean_name(self):
        # custom validation for the name field
        ...

Для этого передайте вашу форму в функцию фабрики:

AuthorFormSet = modelformset_factory(Author, form=AuthorForm)

Вам не обязательно создавать свою форму. Функция modelformset_factory принимает различный аргументы, которые будут переданы в modelform_factory.

Указываем виджеты полей с помощью widgets

Аргумент widgets позволяет указать словарь переопределяющий виджеты для полей ModelForm. Он работает аналогично параметру widgets класса Meta ModelForm:

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title'),
...     widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20})})

Включить локализацию для полей с помощью localized_fields

Используя localized_fields можно включить локализацию для полей формы.

>>> AuthorFormSet = modelformset_factory(
...     Author, fields=('name', 'title', 'birth_date'),
...     localized_fields=('birth_date',))

Если в localized_fields указать '__all__', будут локализированы все поля.

Передача начальных значений

As with regular formsets, it’s possible to specify initial data for forms in the formset by specifying an initial parameter when instantiating the model formset class returned by modelformset_factory(). However, with model formsets, the initial values only apply to extra forms, those that aren’t attached to an existing model instance. If the length of initial exceeds the number of extra forms, the excess initial data is ignored. If the extra forms with initial data aren’t changed by the user, they won’t be validated or saved.

Сохранение объектов набора форм

As with a ModelForm, you can save the data as a model object. This is done with the formset’s save() method:

# Create a formset instance with POST data.
>>> formset = AuthorFormSet(request.POST)

# Assuming all is valid, save the data.
>>> instances = formset.save()

Метод save() возвращает экземпляры объектов, которые были сохранены в базе данных. Те объекты, данные которых не изменились, не сохраняются в базе данных и не отображаются в возвращаемом значении (instances из предыдущего примера).

Когда форма содержит не все поля модели (например, потому что некоторые из них были явно исключены), то отсутствующие поля не будут сохранены через метод save(). Подробнее об этом ограничении модельных форм написано в Указываем какие поля использовать.

Pass commit=False to return the unsaved model instances:

# don't save to the database
>>> instances = formset.save(commit=False)
>>> for instance in instances:
...     # do something with instance
...     instance.save()

Это позволяет вам добавлять данные к экземплярам моделей перед их сохранением в базе данных. Если ваш набор форм содержит ManyToManyField, вам также потребуется вызвать метод formset.save_m2m() для того, чтобы обеспечить сохранение связей «многие-ко-многим».

После вызова save(), в класс набора форм будут добавлены атрибуты, содержащие все изменения:

models.BaseModelFormSet.changed_objects
models.BaseModelFormSet.deleted_objects
models.BaseModelFormSet.new_objects

Ограничение количества редактируемых объектов

Как и в случае набора обычных форм, вы можете использовать аргументы max_num и extra функции modelformset_factory() для ограничения числа дополнительно отображаемых форм.

Аргумент max_num не препятствует отображению существующих объектов:

>>> Author.objects.order_by('name')
<QuerySet [<Author: Charles Baudelaire>, <Author: Paul Verlaine>, <Author: Walt Whitman>]>

>>> AuthorFormSet = modelformset_factory(Author, fields=('name',), max_num=1)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> [x.name for x in formset.get_queryset()]
['Charles Baudelaire', 'Paul Verlaine', 'Walt Whitman']

Also, extra=0 doesn’t prevent creation of new model instances as you can add additional forms with JavaScript or send additional POST data. Formsets don’t yet provide functionality for an «edit only» view that prevents creation of new instances.

Если значение max_num больше чем количество существующих объектов, то к будет добавлено extra пустых форм к набору. Так будет происходить до достижения максимального количества форм, ограниченного параметром max_num:

>>> AuthorFormSet = modelformset_factory(Author, fields=('name',), max_num=4, extra=2)
>>> formset = AuthorFormSet(queryset=Author.objects.order_by('name'))
>>> for form in formset:
...     print(form.as_table())
<tr><th><label for="id_form-0-name">Name:</label></th><td><input id="id_form-0-name" type="text" name="form-0-name" value="Charles Baudelaire" maxlength="100"><input type="hidden" name="form-0-id" value="1" id="id_form-0-id"></td></tr>
<tr><th><label for="id_form-1-name">Name:</label></th><td><input id="id_form-1-name" type="text" name="form-1-name" value="Paul Verlaine" maxlength="100"><input type="hidden" name="form-1-id" value="3" id="id_form-1-id"></td></tr>
<tr><th><label for="id_form-2-name">Name:</label></th><td><input id="id_form-2-name" type="text" name="form-2-name" value="Walt Whitman" maxlength="100"><input type="hidden" name="form-2-id" value="2" id="id_form-2-id"></td></tr>
<tr><th><label for="id_form-3-name">Name:</label></th><td><input id="id_form-3-name" type="text" name="form-3-name" maxlength="100"><input type="hidden" name="form-3-id" id="id_form-3-id"></td></tr>

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

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

Наборы модельных форм во многом похожи на наборы обычных форм. Для отображения набора форм для редактирования экземпляров модели Author:

from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author

def manage_authors(request):
    AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
    if request.method == 'POST':
        formset = AuthorFormSet(request.POST, request.FILES)
        if formset.is_valid():
            formset.save()
            # do something.
    else:
        formset = AuthorFormSet()
    return render(request, 'manage_authors.html', {'formset': formset})

Как вы можете видеть, логика представления не сильно отличается отличается логики обычного набора. Отличием является вызов formset.save() для сохранения данных. (Это было описано ранее в Сохранение объектов набора форм.)

Переопределение clean() у ModelFormSet

Подобно ModelForms, по умолчанию метод clean() класса ModelFormSet будет проверять все данные на нарушение ограничений уникальности, определённых в вашей модели (unique, unique_together или unique_for_date|month|year). Желая сохранить данный функционал при переопределении метода clean(), следует вызывать метод clean() базового класса:

from django.forms import BaseModelFormSet

class MyModelFormSet(BaseModelFormSet):
    def clean(self):
        super().clean()
        # example custom validation across forms in the formset
        for form in self.forms:
            # your custom formset validation
            ...

На этом этапе уже будут созданы экземпляры модели для каждой формы. Поменяв form.cleaned_data, вы не поменяете сохраняемые значения. Для этого в ModelFormSet.clean() необходимо изменить form.instance:

from django.forms import BaseModelFormSet

class MyModelFormSet(BaseModelFormSet):
    def clean(self):
        super().clean()

        for form in self.forms:
            name = form.cleaned_data['name'].upper()
            form.cleaned_data['name'] = name
            # update the instance value.
            form.instance.name = name

Использование собственной выборки

Как было сказано ранее, в можете переопределить стандартную выборку, которая используется набором модельных форм:

from django.forms import modelformset_factory
from django.shortcuts import render
from myapp.models import Author

def manage_authors(request):
    AuthorFormSet = modelformset_factory(Author, fields=('name', 'title'))
    if request.method == "POST":
        formset = AuthorFormSet(
            request.POST, request.FILES,
            queryset=Author.objects.filter(name__startswith='O'),
        )
        if formset.is_valid():
            formset.save()
            # Do something.
    else:
        formset = AuthorFormSet(queryset=Author.objects.filter(name__startswith='O'))
    return render(request, 'manage_authors.html', {'formset': formset})

Следует отметить, что мы передаём аргумент queryset в обе ветки POST и GET в этом примере.

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

Существует три способа отображения набора форм в шаблоне Django.

Во-первых, вы можете позволить набору форм самому сделать всю работу:

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

Во-вторых, в можете вручную вывести набор форм, но позволить его формам отображаться самостоятельно:

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

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

В-третьих, вы можете вывести все поля:

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

Если вы предпочтёте третий способ и не будете использовать {% for %} для итерации по полям, то вам понадобится вывести поле для первичного ключа. Рассмотрим случай, когда требуется вывести поля name и age модели:

<form method="post">
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.id }}
        <ul>
            <li>{{ form.name }}</li>
            <li>{{ form.age }}</li>
        </ul>
    {% endfor %}
</form>

Обратите внимание на то, как мы явно выводим {{ form.id }}. Это гарантирует, что набор модельных форм, в случае POST, будет работать правильно. (Этот пример предполагает, что первичный ключ имеет имя id. Если вы изменили имя первичного ключа, то учтите это в данном примере.)

Встраиваемые наборы форм

class models.BaseInlineFormSet

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

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)

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

>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',))
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)

BookFormSet’s prefix is 'book_set' (<model name>_set ). If Book’s ForeignKey to Author has a related_name, that’s used instead.

Примечание

inlineformset_factory() использует modelformset_factory() и устанавливает can_delete=True.

Переопределение методов в InlineFormSet

Переопределяя методы InlineFormSet, лучше наследоваться от BaseInlineFormSet, чем от BaseModelFormSet.

Например, если вы хотите переопределить clean():

from django.forms import BaseInlineFormSet

class CustomInlineFormSet(BaseInlineFormSet):
    def clean(self):
        super().clean()
        # example custom validation across forms in the formset
        for form in self.forms:
            # your custom formset validation
            ...

Смотрите также Переопределение clean() у ModelFormSet.

Потом при создании набора форм укажите аргумент formset:

>>> from django.forms import inlineformset_factory
>>> BookFormSet = inlineformset_factory(Author, Book, fields=('title',),
...     formset=CustomInlineFormSet)
>>> author = Author.objects.get(name='Mike Royko')
>>> formset = BookFormSet(instance=author)

Более одного внешнего ключа к одной модели

Если ваша модель имеет больше одного внешнего ключа на одну и ту же модель, вам следует разрешить эту путаницу, указав fk_name. Например, рассмотрим следующую модель:

class Friendship(models.Model):
    from_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='from_friends',
    )
    to_friend = models.ForeignKey(
        Friend,
        on_delete=models.CASCADE,
        related_name='friends',
    )
    length_in_months = models.IntegerField()

Чтобы разрешить эту неопределенность, вы можете использовать fk_name в inlineformset_factory():

>>> FriendshipFormSet = inlineformset_factory(Friend, Friendship, fk_name='from_friend',
...     fields=('to_friend', 'length_in_months'))

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

Вам может понадобиться создать представление, которое позволит пользователю редактировать связанные объекты модели. Вот как это можно сделать:

def manage_books(request, author_id):
    author = Author.objects.get(pk=author_id)
    BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
    if request.method == "POST":
        formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
        if formset.is_valid():
            formset.save()
            # Do something. Should generally end with a redirect. For example:
            return HttpResponseRedirect(author.get_absolute_url())
    else:
        formset = BookInlineFormSet(instance=author)
    return render(request, 'manage_books.html', {'formset': formset})

Следует отметить, что мы передаём instance в обоих (POST и GET) случаях.

Определение виджетов используемых в наборе форм

inlineformset_factory использует modelformset_factory и передает большинство аргументов в modelformset_factory. Вы можете использовать параметр widgets``как и для ``modelformset_factory. Смотрите Specifying widgets to use in the form with widgets выше.