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

ModelForm

class ModelForm[исходный код]

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

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

Например:

>>> 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, если editable установлен в True на поле модели, иначе не представлен в форме.
BooleanField BooleanField, или NullBooleanField, если 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
JSONField JSONField
ManyToManyField ModelMultipleChoiceField (см. ниже)
NullBooleanField NullBooleanField
PositiveBigIntegerField IntegerField
PositiveIntegerField IntegerField
PositiveSmallIntegerField IntegerField
SlugField SlugField
SmallAutoField Не представлены в форме
SmallIntegerField IntegerField
TextField CharField с widget=forms.Textarea
TimeField TimeField
URLField URLField
UUIDField UUIDField

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

  • ForeignKey представлен django.forms.ModelChoiceField, который является ChoiceField, выбор которого представляет собой модель QuerySet.
  • ManyToManyField представлен django.forms.ModelMultipleChoiceField, который является MultipleChoiceField, выбор которого представляет собой модель QuerySet.

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

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

Наконец, обратите внимание, что вы можете переопределить поле формы, используемое для данного поля модели. См. Overriding the default fields ниже.

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

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

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. Validating the form
  2. Validating the model instance

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

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

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

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

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

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

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

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

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

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

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

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

Метод модели clean() будет вызван до того, как будут произведены проверки на уникальность. Более подробную информацию о хуке модели Validating objects смотрите в clean().

Соображения относительно модели error_messages

Сообщения об ошибках, определенные на уровне form field или на уровне form Meta, всегда имеют приоритет над сообщениями об ошибках, определенными на уровне model field.

Сообщения об ошибках, определенные на model fields, используются только тогда, когда на шаге ValidationError поднимается вопрос model validation и на уровне формы не определены соответствующие сообщения об ошибках.

Вы можете переопределить сообщения об ошибках 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()

Каждый ModelForm также имеет метод save(). Этот метод создает и сохраняет объект базы данных из данных, привязанных к форме. Подкласс ModelForm может принимать существующий экземпляр модели в качестве аргумента ключевого слова instance; если оно предоставлено, save() обновит этот экземпляр. Если он не указан, save() создаст новый экземпляр указанной модели:

>>> 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()

Обратите внимание, что если форма hasn’t been validated, вызов 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() на полученном экземпляре модели. Это полезно, если вы хотите выполнить пользовательскую обработку объекта перед его сохранением, или если вы хотите использовать один из специализированных model saving options. По умолчанию commit является True.

Другой побочный эффект использования commit=False проявляется, когда ваша модель имеет отношение «многие-ко-многим» с другой моделью. Если ваша модель имеет отношение «многие-ко-многим» и вы указываете commit=False при сохранении формы, Django не может немедленно сохранить данные формы для отношения «многие-ко-многим». Это происходит потому, что невозможно сохранить данные отношения «многие-ко-многим» для экземпляра, пока этот экземпляр не существует в базе данных.

Чтобы обойти эту проблему, каждый раз, когда вы сохраняете форму с помощью commit=False, Django добавляет метод save_m2m() в ваш подкласс ModelForm. После того, как вы вручную сохранили экземпляр, созданный формой, вы можете вызвать save_m2m() для сохранения данных формы «многие-ко-многим». Например:

# 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()

Вызов save_m2m() требуется только в том случае, если вы используете save(commit=False). Когда вы используете save() на форме, все данные, включая данные «многие ко многим», сохраняются без необходимости дополнительных вызовов методов. Например:

# 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 работает точно так же, как и любая другая forms форма. Например, метод 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 имеет 3 поля name, title и birth_date, это приведет к тому, что на форме будут присутствовать поля name и birth_date.

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

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

Примечание

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

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

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

Переопределение полей по умолчанию

Типы полей по умолчанию, описанные в таблице Field types выше, являются разумными значениями по умолчанию. Если у вас есть 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}),
        }

Словарь widgets принимает либо экземпляры виджетов (например, Textarea(...)), либо классы (например, Textarea). Обратите внимание, что словарь widgets игнорируется для поля модели с непустым атрибутом choices. В этом случае вы должны переопределить поле формы, чтобы использовать другой виджет.

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

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

Более подробную информацию о полях и их аргументах см. в form field documentation.

Обеспечение локализации полей

По умолчанию поля в 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__', все поля будут локализованы.

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

Как и в случае с базовыми формами, вы можете расширять и повторно использовать ModelForms, наследуя их. Это полезно, если вам нужно объявить дополнительные поля или дополнительные методы в родительском классе для использования в ряде форм, производных от моделей. Например, используя предыдущий 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, если он существует, иначе 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

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

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

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

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

Поля для включения можно указать с помощью аргументов fields и exclude ключевых слов или соответствующих атрибутов внутреннего ModelForm класса Meta. Обратитесь к документации ModelForm Выбор полей для использования.

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

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

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

class models.BaseModelFormSet

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

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

Использование fields ограничивает набор форм использованием только заданных полей. В качестве альтернативы можно использовать подход «opt-out», указав, какие поля следует исключить:

>>> 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-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS"><input type="hidden" name="form-MAX_NUM_FORMS" value="1000" 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() использует formset_factory() для генерации наборов форм. Это означает, что модельный набор форм является расширением базового набора форм, который знает, как взаимодействовать с конкретной моделью.

Примечание

При использовании multi-table inheritance формы, сгенерированные фабрикой форм, будут содержать поле родительской ссылки (по умолчанию <parent_model_name>_ptr) вместо поля id.

Изменение набора queryset

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

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

В качестве альтернативы можно создать подкласс, который устанавливает self.queryset в __init__:

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)

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

>>> 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__', все поля будут локализованы.

Предоставление начальных значений

Как и в обычных наборах форм, можно specify initial data для форм в наборе форм, указав параметр initial при инстанцировании класса набора форм модели, возвращаемого modelformset_factory(). Однако в модельных наборах форм начальные значения применяются только к дополнительным формам, тем, которые не прикреплены к существующему экземпляру модели. Если длина initial превышает количество дополнительных форм, избыточные начальные данные игнорируются. Если дополнительные формы с начальными данными не были изменены пользователем, они не будут проверены или сохранены.

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

Как и в случае с ModelForm, вы можете сохранить данные как объект модели. Это делается с помощью метода save() набора форм:

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

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

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

Если поля отсутствуют в форме (например, потому что они были исключены), эти поля не будут установлены методом save(). Более подробную информацию об этом ограничении, которое также действует для обычных ModelForms, можно найти в Selecting the fields to use.

Передайте commit=False, чтобы вернуть несохраненные экземпляры модели:

# 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']

Также extra=0 не препятствует созданию новых экземпляров модели, как это можно сделать add additional forms with JavaScript, или отправке дополнительных POST-данных. Наборы форм don’t yet provide functionality для представления «только редактирование», которое предотвращает создание новых экземпляров.

Если значение 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() на ModelFormSet и сохранить эту проверку, вы должны вызвать метод 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. Изменения значения в 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>

Если вы сами вручную выводите формы, убедитесь, что форма управления выводится так, как показано выше. Смотрите management form documentation.

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

<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. Если вы явно определили свой собственный первичный ключ, который не называется id, убедитесь, что он будет отображен).

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

class models.BaseInlineFormSet

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

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 ). Если у Book от ForeignKey до Author есть related_name, то он используется вместо него.

Примечание

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 выше.

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