Spec-Zone.ru › Django 4.2

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

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

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

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

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

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

Учёт сообщений об ошибках модели

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

Например, если вы хотите использовать 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,
        }

или:

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


def formfield_for_dbfield(db_field, **kwargs):
    if db_field.name == "slug":
        return MySlugFormField()
    return db_field.formfield(**kwargs)


class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ["pub_date", "headline", "content", "reporter", "slug"]
        formfield_callback = formfield_for_dbfield

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

См. документацию по полям форм для получения дополнительной информации о полях и их аргументах.

Изменено в Django 4.2:

Добавлен атрибут Meta.formfield_callback.

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

По умолчанию поля в 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

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

Примечание

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

Предотвращение создания новых объектов

Новое в Django 4.1.

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

>>> AuthorFormSet = modelformset_factory(
...     Author,
...     fields=["name", "title"],
...     edit_only=True,
... )

Здесь набор форм будет только редактировать существующие экземпляры Author. Никакие другие объекты не будут созданы или изменены.

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

Наборы форм модели очень похожи на наборы форм. Предположим, что мы хотим отобразить набор форм для редактирования экземпляров модели 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() в 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"])
    queryset = Author.objects.filter(name__startswith="O")
    if request.method == "POST":
        formset = AuthorFormSet(
            request.POST,
            request.FILES,
            queryset=queryset,
        )
        if formset.is_valid():
            formset.save()
            # Do something.
    else:
        formset = AuthorFormSet(queryset=queryset)
    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’s префикс равен 'book_set' (<model name>_set ). Если Book’s 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() в 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. См. Указание виджетов для использования в форме с виджетами выше.

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

Spec-Zone.ru

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