Формы в Django

⏱ 30 мин · 🧩 заданий: 6

Зачем формы

Обработка request.POST вручную требует много рутины: проверить, что поля заполнены, что email похож на email, что число — число; при ошибке показать форму снова с сообщениями и введёнными данными. Django-формы делают это за вас:

  • генерируют HTML полей;
  • проверяют (валидируют) данные;
  • приводят их к типам Python;
  • показывают ошибки.

Класс Form

library/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(choices=[("q", "Вопрос"), ("bug", "Ошибка")])
    message = forms.CharField(label="Сообщение", widget=forms.Textarea(attrs={"rows": 5}))
    copy = forms.BooleanField(label="Отправить копию мне", required=False)

Поля формы похожи на поля модели: CharField, IntegerField, EmailField, DateField, ChoiceField, BooleanField, FileField… Все поля по умолчанию обязательны — required=False делает поле необязательным.

Виджет определяет HTML-элемент: TextInput, Textarea, Select, CheckboxInput, PasswordInput, DateInput… Через attrs можно добавить HTML-атрибуты (например, CSS-классы Bootstrap).

Обработка во view

from django.contrib import messages
from django.shortcuts import redirect, render

from .forms import ContactForm


def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)       # «связанная» форма с данными
        if form.is_valid():
            data = form.cleaned_data
            send_mail(data["subject"], data["message"], data["email"], ["admin@site.ru"])
            messages.success(request, "Сообщение отправлено!")
            return redirect("contact")         # Post/Redirect/Get
    else:
        form = ContactForm()                   # пустая форма
    return render(request, "contact.html", {"form": form})

Логика:

  1. GET — показываем пустую форму.
  2. POST — заполняем форму данными и проверяем is_valid().
  3. Если всё хорошо — делаем дело и перенаправляем (чтобы F5 не отправил форму повторно).
  4. Если есть ошибки — снова рендерим ту же форму: она уже содержит введённые данные и сообщения об ошибках.

Вывод в шаблоне

Быстро:

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">Отправить</button>
</form>

Варианты: {{ form }} (с Django 5 — разметка на div), form.as_p, form.as_ul, form.as_table.

Вручную — для полного контроля над вёрсткой:

<form method="post" novalidate>
  {% 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 %}
      {% for error in field.errors %}<div class="error">{{ error }}</div>{% endfor %}
    </div>
  {% endfor %}
  <button type="submit">Отправить</button>
</form>

Так, например, сделаны формы входа и регистрации на этом сайте.

CSRF

{% csrf_token %} добавляет скрытое поле с секретным токеном. Django проверяет его у каждого POST-запроса — это защита от CSRF-атак, когда чужой сайт заставляет браузер пользователя отправить форму на ваш сайт. Без токена — ошибка 403.

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

Если форма создаёт или редактирует объект модели, не нужно описывать поля заново (помните DRY?):

from .models import Book


class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ["title", "author", "pages", "description"]   # или "__all__" (не рекомендуется)
        labels = {"title": "Название"}
        widgets = {"description": forms.Textarea(attrs={"rows": 4})}
        help_texts = {"pages": "Целое число"}

ForeignKey автоматически станет выпадающим списком, ManyToManyField — множественным выбором.

def book_create(request):
    form = BookForm(request.POST or None)
    if request.method == "POST" and form.is_valid():
        book = form.save()          # создаёт объект в базе
        return redirect(book)
    return render(request, "library/book_form.html", {"form": form})


def book_update(request, pk):
    book = get_object_or_404(Book, pk=pk)
    form = BookForm(request.POST or None, instance=book)   # instance — редактируем существующий
    if request.method == "POST" and form.is_valid():
        form.save()
        return redirect(book)
    return render(request, "library/book_form.html", {"form": form})

form.save(commit=False) создаёт объект, но не сохраняет его — чтобы дописать поля, которых нет в форме:

post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m()   # если есть ManyToMany-поля

Валидация

Встроенная

Поля сами проверяют тип, max_length, required, формат email, уникальность (unique=True у модели в ModelForm)…

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

class BookForm(forms.ModelForm):
    ...

    def clean_title(self):
        title = self.cleaned_data["title"].strip()
        if title.lower() == "без названия":
            raise forms.ValidationError("Придумайте название")
        return title          # обязательно вернуть значение!

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

class RegisterForm(forms.Form):
    password = forms.CharField(widget=forms.PasswordInput)
    password2 = forms.CharField(widget=forms.PasswordInput)

    def clean(self):
        cleaned = super().clean()
        if cleaned.get("password") != cleaned.get("password2"):
            raise forms.ValidationError("Пароли не совпадают")
        return cleaned

Ошибка из clean() попадает в form.non_field_errors. Чтобы привязать её к конкретному полю — self.add_error("password2", "Пароли не совпадают").

Загрузка файлов

<form method="post" enctype="multipart/form-data">
form = DocumentForm(request.POST, request.FILES)

Без enctype и request.FILES файлы не дойдут до сервера.

Итог

  • forms.Form — произвольная форма, forms.ModelForm — форма из модели.
  • Шаблон обработки: GET → пустая форма; POST → is_valid() → действие → redirect.
  • Данные после проверки — в form.cleaned_data.
  • Своя валидация: clean_<поле>() и clean().
  • В POST-форме всегда {% csrf_token %}.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Создайте форму ContactForm (наследник forms.Form) с полями: name — строка до 100 символов, email — EmailField, message — строка с виджетом Textarea.

Задание 2 Написать код

Создайте BookForm на основе модели Book (ModelForm) с полями title, author, pages.

Задание 3 Расставить по порядку

Расставьте строки типичной обработки формы во view.

  1. else:
  2. if request.method == "POST":
  3. if form.is_valid():
  4. form = BookForm()
  5. form.save()
  6. return redirect("library:book_list")
  7. return render(request, "library/book_form.html", {"form": form})
  8. form = BookForm(request.POST)
Задание 4 Один вариант ответа

Где после успешной проверки is_valid() лежат очищенные данные формы?

Задание 5 Написать код

Добавьте в BookForm метод проверки поля pages: если значение меньше 1, выбросьте forms.ValidationError("Слишком мало страниц"), иначе верните значение.

Задание 6 Один вариант ответа

Зачем в POST-форме нужен {% csrf_token %}?

Войдите, чтобы отмечать пройденные уроки.