Django-Admin.ru

Формы в Django: Form и ModelForm

Как работать с формами в Django: Form и ModelForm, валидация clean и clean_field, вывод в шаблоне, CSRF, обработка во view и оформление через crispy forms.

Редакция Django-Admin.ru
· обновлено · 10 мин чтения

Содержание · 9 разделов
  1. Класс Form: поля и виджеты
  2. Валидация: is_valid, clean_field и clean
  3. Проверка одного поля: clean_<field>()
  4. Проверка нескольких полей: clean
  5. ModelForm: форма из модели
  6. Обработка формы во view
  7. Вывод формы в шаблоне и CSRF
  8. Оформление через django-crispy-forms
  9. Частые ошибки
  10. Форма всегда невалидна, а ошибок не видно
  11. KeyError в clean()
  12. 403 CSRF verification failed
  13. Файл не загружается, поле пустое
  14. TemplateSyntaxError: Invalid filter: ‘crispy’
  15. Частые вопросы
  16. Как задать начальные значения полей?
  17. Как вывести поле даты с календарём браузера?
  18. Как заполнить choices динамически или передать в форму пользователя?
  19. Как сделать поле только для чтения?
  20. Как обработать несколько одинаковых форм на одной странице?
  21. Итог

Формы в Django (django forms) описываются классом: поля, виджеты и правила проверки. Если форма просто собирает данные (обратная связь, поиск), наследуйтесь от forms.Form. Если она создаёт или редактирует запись модели — от forms.ModelForm: поля возьмутся из модели, а form.save() сохранит объект в базу.

from django import forms

class ContactForm(forms.Form):
    email = forms.EmailField()
    message = forms.CharField(widget=forms.Textarea)

form = ContactForm(request.POST)
if form.is_valid():
    form.cleaned_data["email"]  # проверенные и приведённые к типу данные

Ниже — весь цикл работы с формой: описание полей, валидация через clean() и clean_<field>(), обработка во view, вывод в шаблоне с защитой CSRF и оформление через django-crispy-forms. Примеры проверены на Django 5.2 и 6.x.

Класс Form: поля и виджеты

Создайте файл forms.py в приложении (в примерах — blog, как его создать — в статье про startapp):

# blog/forms.py
from django import forms


class ContactForm(forms.Form):
    name = forms.CharField(label="Имя", max_length=100)
    email = forms.EmailField(label="Email")
    subject = forms.ChoiceField(
        label="Тема",
        choices=[("question", "Вопрос"), ("bug", "Ошибка на сайте")],
    )
    message = forms.CharField(label="Сообщение", widget=forms.Textarea)
    copy_me = forms.BooleanField(label="Отправить копию мне", required=False)

Поле отвечает за проверку и приведение типа: EmailField проверит адрес, IntegerField вернёт int, DateField — datetime.date. Виджет отвечает за HTML: CharField по умолчанию рисуется как <input type="text">, а с widget=forms.Textarea — как <textarea>.

Частые поля:

ПолеЧто принимаетВиджет по умолчанию
CharFieldСтроку; max_length, stripTextInput
EmailField, URLFieldEmail, URLEmailInput, URLInput
IntegerField, DecimalFieldЧисла; min_value, max_valueNumberInput
DateField, DateTimeFieldДату, дату и времяDateInput, DateTimeInput
BooleanFieldГалочку; нужен required=FalseCheckboxInput
ChoiceField, MultipleChoiceFieldЗначения из choicesSelect, SelectMultiple
FileField, ImageFieldЗагруженный файлClearableFileInput

Все поля по умолчанию обязательные (required=True). Для BooleanField это значит «галочка должна стоять», поэтому необязательные чекбоксы помечают required=False.

HTML-атрибуты задаются через виджет:

name = forms.CharField(
    widget=forms.TextInput(attrs={"class": "input", "placeholder": "Ваше имя"}),
)
# → <input type="text" name="name" class="input" placeholder="Ваше имя" required id="id_name">

Валидация: is_valid, clean_field и clean

Форма бывает несвязанной (ContactForm() — пустая, для показа) и связанной (ContactForm(request.POST) — с данными пользователя). Проверку запускает is_valid(): он прогоняет каждое поле, затем ваши методы clean_<поле>(), затем общий clean(). Результат лежит в двух атрибутах:

  • form.cleaned_data — словарь проверенных значений, только для полей без ошибок;
  • form.errors — ошибки по полям.

Проверка одного поля: clean_<field>()

Метод с именем clean_<имя_поля> получает уже приведённое значение из self.cleaned_data, проверяет его и обязательно возвращает результат — он и попадёт в cleaned_data:

from django.core.exceptions import ValidationError


class ContactForm(forms.Form):
    ...

    def clean_name(self):
        name = self.cleaned_data["name"].strip()
        if len(name) < 2:
            raise ValidationError("Имя слишком короткое.")
        return name

Если забыть return, значение поля станет None.

Проверка нескольких полей: clean

Когда правило связывает поля между собой, переопределите clean(). Обращайтесь к полям через .get(): поле с ошибкой в cleaned_data отсутствует.

    def clean(self):
        cleaned_data = super().clean()
        subject = cleaned_data.get("subject")
        message = cleaned_data.get("message", "")
        if subject == "bug" and "http" not in message:
            self.add_error("message", "Укажите ссылку на страницу с ошибкой.")
        return cleaned_data

add_error("message", ...) привязывает ошибку к полю. raise ValidationError(...) внутри clean() или add_error(None, ...) создают ошибку всей формы — она доступна через form.non_field_errors().

Проверим в python manage.py shell:

>>> f = ContactForm(data={"name": " A ", "email": "bad", "subject": "bug", "message": "сломалось"})
>>> f.is_valid()
False
>>> f.errors.as_data()
{'name': [ValidationError(['Имя слишком короткое.'])],
 'email': [ValidationError(['Enter a valid email address.'])],
 'message': [ValidationError(['Укажите ссылку на страницу с ошибкой.'])]}
>>> f.cleaned_data
{'subject': 'bug', 'copy_me': False}

Встроенные сообщения выводятся на языке из LANGUAGE_CODE: с "ru" вместо Enter a valid email address. будет русский текст.

ModelForm: форма из модели

Для создания и редактирования записей описывать поля второй раз не нужно. Пусть есть модель (подробнее о полях — в статье «Модели в Django»):

# blog/models.py
class Post(models.Model):
    title = models.CharField("Заголовок", max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    body = models.TextField("Текст")
    is_published = models.BooleanField("Опубликован", default=False)
# blog/forms.py
from .models import Post


class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ["title", "slug", "body", "is_published"]
        labels = {"slug": "Адрес (slug)"}
        help_texts = {"slug": "Латиница, цифры и дефисы."}
        widgets = {"body": forms.Textarea(attrs={"rows": 8})}

    def clean_title(self):
        title = self.cleaned_data["title"]
        if title.isupper():
            raise ValidationError("Не пишите заголовок капслоком.")
        return title

Что ModelForm делает сама:

  • строит поля по модели: max_length, blank, choices и verbose_name переходят в форму;
  • проверяет ограничения модели, включая unique: для занятого slug вернётся Post with this Slug already exists.;
  • сохраняет объект: form.save() создаёт запись, а форма с instance=post её обновляет.

Список полей задавайте явно через fields. С fields = "__all__" любое новое поле модели (например, is_staff у пользователя) автоматически станет редактируемым — так пользователь сможет изменить данные, которые ему менять нельзя.

Если перед сохранением нужно заполнить поля, которых нет в форме, используйте commit=False:

post = form.save(commit=False)   # объект создан, но не записан в базу
post.author = request.user
post.save()
form.save_m2m()                  # нужно, только если в форме есть ManyToMany-поля

Обработка формы во view

Классический шаблон: на GET показываем пустую форму, на POST проверяем данные, при успехе делаем редирект, при ошибке показываем форму снова — уже с ошибками и введёнными значениями.

# blog/views.py
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render

from .forms import ContactForm, PostForm
from .models import Post


def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            data = form.cleaned_data
            # отправить письмо, сохранить заявку и т. д.
            messages.success(request, "Сообщение отправлено.")
            return redirect("contact")
    else:
        form = ContactForm()
    return render(request, "blog/contact.html", {"form": form})


def post_edit(request, pk):
    post = get_object_or_404(Post, pk=pk)
    form = PostForm(request.POST or None, instance=post)
    if request.method == "POST" and form.is_valid():
        form.save()
        return redirect("post_edit", pk=post.pk)
    return render(request, "blog/post_form.html", {"form": form, "post": post})

Редирект после успешного POST (паттерн Post/Redirect/Get) защищает от повторной отправки при обновлении страницы. Для загрузки файлов передайте в форму и request.FILES: PostForm(request.POST, request.FILES), а в теге <form> укажите enctype="multipart/form-data".

Для типовых страниц создания и редактирования есть готовые классы CreateView и UpdateView — они делают то же самое по атрибутам model и fields или form_class.

Вывод формы в шаблоне и CSRF

Самый короткий вариант — вывести форму целиком:

<!-- blog/templates/blog/contact.html -->
<form method="post">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Отправить</button>
</form>

{{ form }} рисует поля в <div> с подписями и ошибками (в Django 5.0+; в 4.x был табличный вывод, а {{ form.as_div }} доступен с 4.1). Есть и другие варианты: {{ form.as_p }}, {{ form.as_ul }}, {{ form.as_table }}.

Для своей разметки выводите поля циклом:

<form method="post">
  {% csrf_token %}
  {{ form.non_field_errors }}
  {% for field in form %}
  <div class="field">
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
    {{ field.errors }}
  </div>
  {% endfor %}
  <button type="submit">Сохранить</button>
</form>

К конкретному полю можно обратиться по имени: {{ form.email }}, {{ form.email.errors }}, {{ form.email.label_tag }}. О синтаксисе шаблонов, циклах и фильтрах — в статье «Шаблоны Django».

{% csrf_token %} обязателен в каждой POST-форме. Он добавляет скрытое поле с токеном, а CsrfViewMiddleware сверяет его с cookie. Без тега Django ответит 403 Forbidden с сообщением «CSRF verification failed». Для шаблона токен доступен, когда view использует render(request, ...) — функция передаёт request в шаблон. GET-формы (поиск, фильтры) токена не требуют: они не меняют данные.

Оформление через django-crispy-forms

Писать разметку Bootstrap для каждого поля вручную утомительно. Пакет django-crispy-forms рендерит форму готовыми шаблонами CSS-фреймворка. Начиная с версии 2.0 шаблоны вынесены в отдельные пакеты — для Bootstrap 5 это crispy-bootstrap5:

pip install django-crispy-forms crispy-bootstrap5
# config/settings.py
INSTALLED_APPS = [
    # ...
    "crispy_forms",
    "crispy_bootstrap5",
    "blog",
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

В шаблоне есть два способа. Фильтр |crispy рендерит только поля, тег <form>, токен и кнопку вы пишете сами:

{% load crispy_forms_tags %}
<form method="post">
  {% csrf_token %}
  {{ form|crispy }}
  <button type="submit" class="btn btn-primary">Сохранить</button>
</form>

Тег {% crispy form %} выводит форму целиком, включая <form>, CSRF-токен и кнопки. Его настраивают объектом FormHelper в самой форме:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit


class ContactForm(forms.Form):
    ...

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = "post"
        self.helper.add_input(Submit("submit", "Отправить"))

Поля получат классы form-control, form-label, обёртки mb-3 и подсветку ошибок. Сам CSS Bootstrap crispy-forms не подключает — добавьте его в базовый шаблон.

Частые ошибки

Форма всегда невалидна, а ошибок не видно

Обычно шаблон не выводит ошибки: вы рисуете поля вручную без {{ field.errors }} и {{ form.non_field_errors }}. Для отладки распечатайте form.errors во view. Вторая частая причина — во view создаётся новая пустая форма вместо связанной: проверьте, что при ошибке в шаблон уходит тот же объект, на котором вызывали is_valid().

KeyError в clean()

Обращение self.cleaned_data["field"] падает, если поле не прошло собственную проверку. В clean() используйте cleaned_data.get("field").

403 CSRF verification failed

Нет {% csrf_token %} внутри <form>, шаблон отрендерен без request (например, через render_to_string без аргумента request) или сайт работает по HTTPS за прокси без CSRF_TRUSTED_ORIGINS. Последний случай разобран в статье про деплой Django.

Файл не загружается, поле пустое

Не передан request.FILES в форму или у тега <form> нет enctype="multipart/form-data".

TemplateSyntaxError: Invalid filter: ‘crispy’

В шаблоне нет {% load crispy_forms_tags %}. Если же Django пишет 'crispy_forms_tags' is not a registered tag library, пакет не добавлен в INSTALLED_APPS. Ошибки про template pack означают, что не установлен crispy-bootstrap5 или не заданы настройки CRISPY_*.

Частые вопросы

Как задать начальные значения полей?

Параметром initial: для всей формы — словарём при создании, для одного поля — в его описании. Начальные значения показываются только в несвязанной форме и не подставляются в cleaned_data, если пользователь очистил поле.

form = ContactForm(initial={"subject": "question", "email": request.user.email})

Как вывести поле даты с календарём браузера?

Укажите виджету HTML-тип date и формат ISO: браузерный календарь принимает значение только в виде ГГГГ-ММ-ДД.

published = forms.DateField(
    widget=forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d"),
)

Как заполнить choices динамически или передать в форму пользователя?

Добавьте в __init__ свой именованный аргумент и настройте поля после вызова super().__init__():

class SearchForm(forms.Form):
    category = forms.ChoiceField(choices=[])

    def __init__(self, *args, categories=(), **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["category"].choices = [(c, c) for c in categories]


form = SearchForm(request.GET or None, categories=["Django", "FastAPI"])

Для выбора записей из базы удобнее ModelChoiceField с queryset, отфильтрованным по request.user.

Как сделать поле только для чтения?

Поставьте полю disabled = True. Браузер покажет его неактивным, а Django проигнорирует присланное значение и возьмёт исходное из initial или instance — подделать поле через POST не получится.

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    if self.instance.pk:
        self.fields["slug"].disabled = True

Как обработать несколько одинаковых форм на одной странице?

Через набор форм (formset): formset_factory для обычных форм и modelformset_factory для моделей. В шаблоне обязательно выведите {{ formset.management_form }} — без служебных полей Django не поймёт, сколько форм пришло.

from django.forms import formset_factory

LinkFormSet = formset_factory(LinkForm, extra=3)
formset = LinkFormSet(request.POST or None)

Итог

forms.Form — для произвольных данных, forms.ModelForm — для записей модели с form.save(). Проверки пишите в clean_<поле>() (возвращая значение) и clean() (для связей между полями), во view используйте связку GET → POST → is_valid() → редирект, а в шаблоне не забывайте {% csrf_token %}. Для красивой вёрстки подключите crispy-forms. Как сохранённые формой данные потом выбирать из базы, смотрите в шпаргалке по Django ORM; формы входа и регистрации разобраны в статье про аутентификацию в Django.

  • #Django
  • #Django Forms
  • #ModelForm
  • #Валидация
  • #crispy forms
↑ ↓ выборEnter открытьEsc закрыть