Работа с обычными формами (forms.Form) — отличный способ понять, как Django принимает и валидирует данные. Но в реальных проектах очень часто данные формы должны сохраняться в базу данных. Например:
- форма добавления фильма,
- форма добавления актёра,
- форма редактирования жанра,
- форма добавления рецензии.
Если создавать такие формы вручную, каждый раз повторится одно и то же:
- объявить поля формы,
- проверить валидность,
- вручную создать объект модели через
Model.objects.create(), - обрабатывать уникальность, ограничения, ошибки.
Django решает эту проблему одним мощным инструментом — ModelForm.
ModelForm — это класс формы, который автоматически генерирует поля на основе модели.
Он:
- создаёт форму на основе модели;
- автоматически выполняет все её проверки;
- сам знает, как сохранить данные через
form.save(); - экономит огромное количество времени и кода.
Как и раньше — в файле:
cinemahub/forms.py
Если файла ещё нет — создаём.
Предположим, у нас есть модель фильма:
from django.db import models
class Genre(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
year = models.PositiveIntegerField()
is_published = models.BooleanField(default=True)
genre = models.ForeignKey(Genre, on_delete=models.PROTECT)
def __str__(self):
return self.titlefrom django import forms
from .models import Movie, Genre
class MovieForm(forms.ModelForm):
class Meta:
model = Movie
fields = [
"title",
"slug",
"description",
"year",
"is_published",
"genre",
]-
model = Movieговорит Django, с какой моделью связана форма. -
fieldsопределяет, какие поля нужно вывести. -
Django автоматически создаст:
- поля ввода текста,
- textarea,
- checkbox,
- списки выбора,
- валидаторы уникальности,
- валидатор пустых значений,
- типы значений (например, год должен быть числом).
То есть то, что раньше нужно было писать вручную, теперь работает «из коробки».
Обычно формы нужно немного «причесать»: добавить CSS-классы, изменить размеры полей, задать метки.
Для этого внутри Meta используется ключ widgets.
class MovieForm(forms.ModelForm):
class Meta:
model = Movie
fields = ["title", "slug", "description", "year", "is_published", "genre"]
widgets = {
"title": forms.TextInput(attrs={"class": "form-input"}),
"description": forms.Textarea(attrs={"rows": 5, "cols": 60}),
}
labels = {
"slug": "URL фильма",
"year": "Год выхода",
}Поле genre — это ForeignKey. Django отобразит его как <select>. Но иногда хочется добавить вариант «Выберите жанр».
Тогда переопределяем поле:
genre = forms.ModelChoiceField(
queryset=Genre.objects.all(),
empty_label="Жанр не выбран",
label="Жанр",
)Поле при этом нужно вынести наружу класса Meta.
Создадим страницу добавления фильма.
from django.shortcuts import render, redirect
from .forms import MovieForm
def add_movie(request):
if request.method == "POST":
form = MovieForm(request.POST)
if form.is_valid():
form.save() # <-- самое важное!
return redirect("home")
else:
form = MovieForm()
return render(request, "cinemahub/add_movie.html", {"form": form})- Django создаёт объект Movie.
- Переносит в него данные формы.
- Валидирует уникальность
slug. - Сохраняет объект в базу.
ModelForm делает всю работу за нас.
<h1>Добавить фильм</h1>
<form method="post">
{% csrf_token %} {{ form.as_p }}
<button type="submit">Сохранить</button>
</form>Запускаем сервер:
python manage.py runserver
Переходим по адресу:
/add-movie/
Пробуем:
- оставить поля пустыми,
- ввести slug с пробелами,
- указать текст в поле
year, - указать уже существующий slug.
Каждая ошибка будет отображена автоматически — Django всё сделает сам.
Допустим, мы хотим:
- ограничить максимальную длину названия (например, 50 символов)
- запретить использовать числа в названии фильма
Реализуем метод clean_<поле>:
from django.core.exceptions import ValidationError
class MovieForm(forms.ModelForm):
class Meta:
model = Movie
fields = ["title", "slug", "description", "year", "is_published", "genre"]
def clean_title(self):
title = self.cleaned_data["title"]
if len(title) > 50:
raise ValidationError("Название фильма не должно превышать 50 символов")
if any(char.isdigit() for char in title):
raise ValidationError("Название фильма не может содержать цифры")
return titleПричина: пользователь ввёл slug, который уже существует.
Решение: Django автоматически отобразит ошибку под полем slug.
Чаще всего происходит, если:
- неправильно написан метод clean_title
- забыто
return title
Проверьте шаблон: в нём должно быть либо {{ form }}, либо {{ form.as_p }}.
- Создать форму для добавления жанра. Создайте форму
GenreForm, которая:
- связана с моделью Genre
- имеет одно поле
name - ограничивает длину имени жанра до 30 символов (через
clean_name) - добавляет CSS-класс
form-input
Создайте представление add_genre и шаблон add_genre.html, в котором форма выводится через {{ form.as_p }}.
- Проверка уникальности названия фильма. Создайте в форме MovieForm метод
clean_title, который:
- запрещает вводить одинаковые названия разных фильмов (проверяет через Movie.objects.filter)
- Стилизовать форму через widgets. Создайте форму
MovieFormMinimal, в которой:
- выводятся только поля title и year
- поля получают CSS-класс
input-min
- Создать форму для добавления жанра.
cinemahub/forms.py
class GenreForm(forms.ModelForm):
class Meta:
model = Genre
fields = ["name"]
widgets = {
"name": forms.TextInput(attrs={"class": "form-input"}),
}
def clean_name(self):
name = self.cleaned_data["name"]
if len(name) > 30:
raise ValidationError("Длина жанра не может превышать 30 символов")
return namecinemahub/views.py
def add_genre(request):
if request.method == "POST":
form = GenreForm(request.POST)
if form.is_valid():
form.save()
return redirect("home")
else:
form = GenreForm()
return render(request, "cinemahub/add_genre.html", {"form": form})add_genre.html
<h1>Добавить жанр</h1>
<form method="post">
{% csrf_token %} {{ form.as_p }}
<button type="submit">Сохранить</button>
</form>- Проверка уникальности названия фильма.
def clean_title(self):
title = self.cleaned_data["title"]
if Movie.objects.filter(title=title).exists():
raise ValidationError("Фильм с таким названием уже существует")
return title- Стилизовать форму через widgets.
class MovieFormMinimal(forms.ModelForm):
class Meta:
model = Movie
fields = ["title", "year"]
widgets = {
"title": forms.TextInput(attrs={"class": "input-min"}),
"year": forms.NumberInput(attrs={"class": "input-min"}),
}- Что такое ModelForm и в чём его отличие от обычной формы?
- Где в Django указывается связь формы и модели?
- Для чего используется класс Meta?
- Что делает метод
form.save()? - Где лучше размещать кастомные валидаторы для полей формы?
- Когда применяется метод
clean_<имя_поля>? - Что делает атрибут
widgetsв классе Meta? - Как добавить placeholder или CSS-класс к полю формы?
- Как работает проверка уникальности slug?
- Зачем нужен
empty_labelвModelChoiceField?