Spec-Zone.ru › Django 2.2

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

ModelForm

class ModelForm [source]

Если вы создаёте приложение, работающее с базой данных, скорее всего, у вас будут формы, тесно связанные с моделями 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
ManyToManyField ModelMultipleChoiceField (см. ниже)
NullBooleanField NullBooleanField
PositiveIntegerField IntegerField
PositiveSmallIntegerField IntegerField
SlugField SlugField
SmallIntegerField IntegerField
TextField CharField с widget=forms.Textarea
TimeField TimeField
URLField URLField
UUIDField UUIDField
END_OF_DOCUMENT_MARKER

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

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

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

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

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 валидация (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 или на уровне метаданных формы, всегда имеют приоритет над сообщениями об ошибках, определенными на уровне model field.

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

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

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

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

    Например:

    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() модели 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()

Подробнее об использовании 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}),
        }

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

Дополнительную информацию о полях и их аргументах см. в документации по полям форм.

Включение локализации полей

По умолчанию поля в 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 inner Meta. См. документацию по Выбору полей для использования.

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

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

Наборы форм моделей

class models.BaseModelFormSet

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

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

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

>>> 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() использует formset_factory() для генерации наборов форм. Это означает, что набор форм модели — это просто расширение базового набора форм, который знает, как взаимодействовать с конкретной моделью.

Примечание

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

Изменение набора запросов

По умолчанию, при создании набора форм из модели, набор форм будет использовать набор запросов, включающий все объекты в модели (например, 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)

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

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

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

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

Передайте 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 не препятствует созданию новых экземпляров моделей, поскольку вы можете добавить дополнительные формы с помощью JavaScript или просто отправить дополнительные данные POST. Наборы форм пока не предоставляют функциональности для представления «только для редактирования», которая предотвращает создание новых экземпляров.

Если значение 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() набора форм модели будет проверять, что ни один из элементов набора форм не нарушает уникальные ограничения вашей модели (либо 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 уже созданы отдельные экземпляры модели. Изменение значения в 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. Если вы явно определили свой собственный первичный ключ, который не называется 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 — 'book_set' (<model name>_set). Если у Book свойство ForeignKey по отношению к Author имеет related_name, то используется оно.

Примечание

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

См. также

Вручную рендеренные can_delete и can_order.

Переопределение методов в 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() в наборе форм модели.

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

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

Spec-Zone.ru

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