Формы в Django: Form и ModelForm
Как работать с формами в Django: Form и ModelForm, валидация clean и clean_field, вывод в шаблоне, CSRF, обработка во view и оформление через crispy forms.
Содержание · 9 разделов
- Класс Form: поля и виджеты
- Валидация: is_valid, clean_field и clean
- Проверка одного поля: clean_<field>()
- Проверка нескольких полей: clean
- ModelForm: форма из модели
- Обработка формы во view
- Вывод формы в шаблоне и CSRF
- Оформление через django-crispy-forms
- Частые ошибки
- Форма всегда невалидна, а ошибок не видно
- KeyError в clean()
- 403 CSRF verification failed
- Файл не загружается, поле пустое
- TemplateSyntaxError: Invalid filter: ‘crispy’
- Частые вопросы
- Как задать начальные значения полей?
- Как вывести поле даты с календарём браузера?
- Как заполнить choices динамически или передать в форму пользователя?
- Как сделать поле только для чтения?
- Как обработать несколько одинаковых форм на одной странице?
- Итог
Формы в 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, strip | TextInput |
EmailField, URLField | Email, URL | EmailInput, URLInput |
IntegerField, DecimalField | Числа; min_value, max_value | NumberInput |
DateField, DateTimeField | Дату, дату и время | DateInput, DateTimeInput |
BooleanField | Галочку; нужен required=False | CheckboxInput |
ChoiceField, MultipleChoiceField | Значения из choices | Select, 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