🐍🎸 Курс Django. Часть 3: Основы работы с формами
Разбираем основные методы создания, кастомного рендеринга и кастомной валидации форм.
Django формы: основная функциональность
Модуль Forms в Django берет на себя всю сложную работу, связанную с обработкой пользовательского ввода. Вот его основные возможности:
1. Почти автоматическое создание форм на основе моделей. При этом в форму можно включать только нужные поля модели, а не все сразу.
2. Два основных типа форм forms.Form и forms.ModelForm – первый тип обычно используется для обработки данных, которые не сохраняются в модели, второй применяют для сохранения информации в БД.
3. Вывод формы в
шаблоне с помощью тега {{ form }}
. У Django гибкий подход к выводу – форму можно
рендерить несколькими способами, причем последний метод,
как мы увидим позже, предоставляет самые обширные возможности для кастомного дизайна форм с помощью HTML/CSS:
- как параграф
{{ form.as_p }}
- в виде списка
{{ form.as_ul }}
- в форме таблицы
{{ form.as_table }}
- по отдельным полям
{{ form.name_of_field }}
4. Получение и обработка данных из HTML и AJAX-форм, которые не имеют конкретных исходных моделей. Такой подход используется:
- для работы с данными, которые не нужно сохранять в базе данных – например, для обработки контактной формы, данные из которой пересылаются на email.
- при обработке форм со сложным дизайном.
- при работе с AJAX формами, когда данные отправляются на сервер (и приходят с сервера) в фоновом режиме с использованием JavaScript.
5. Валидация полученных с фронтенда данных. При отправке формы данные попадают в объект request. Во view мы берем эти данные через request.POST, создаем экземпляр формы, передав туда данные, и вызываем метод is_valid() для проверки валидности.
6. Сохранение очищенных данных из валидной формы. Очищенные данные из формы можно получить с помощью метода clean(), после чего эти данные можно дополнительно проверить на соответствие нужным критериям и безопасно сохранить в базе данных.
7. Отображение ошибок валидации. Если данные не соответствуют критериям, Django передает в шаблон объект формы и статус валидности. С помощью цикла можно вывести все ошибки списком, либо указать на ошибку рядом с конкретным полем.
Создание форм в Django
Начнем с создания формы для получения данных о пользователе. Поскольку эти данные будут сохраняться в БД, нужно определить соответствующую модель:
from django.db import models class Profile(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) photo = models.ImageField(upload_to='photos') telegram = models.CharField(max_length=20) city = models.CharField(max_length=20) profession = models.CharField(max_length=50) bio = models.TextField() class Meta: ordering = ['id'] verbose_name = 'Профиль' verbose_name_plural = 'Профили' def __str__(self): return f"{self.first_name} {self.last_name}, {self.profession}"
Для хранения ссылки на фото мы используем ImageField, поэтому в вашем виртуальном окружении должна быть библиотека Pillow. Не забудьте создать и применить миграции, чтобы таблица Profile появилась в базе данных.
При выводе формы мы будем использовать все поля модели, поэтому значение fields
равно _all_
:
from django import forms from .models import Profile class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = '__all__' labels = { 'first_name': 'Имя', 'last_name': 'Фамилия', 'photo': 'Фото', 'telegram': 'Телеграм', 'city': 'Город', 'profession': 'Профессия', 'bio': 'О себе', }
Теперь можно написать представление для вывода формы:
from django.shortcuts import render from .forms import ProfileForm def profile_view(request): form = ProfileForm() return render(request, 'profile.html', {'form': form})
Маршрут, по которому будет доступна страница:
from django.urls import path from .views import profile_view urlpatterns = [ path('profile/', profile_view, name='profile'), ]
И шаблон для вывода формы:
<!DOCTYPE html> <html> <head> <title>Профиль пользователя</title> </head> <body> <h2>Заполните форму своими данными:</h2> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit">Сохранить</button> </form> </body> </html>
Запустите сервер, перейдите по адресу http://127.0.0.1:8000/profile/ – форма работает:
Неказистый внешний вид форм, выведенных с помощью базовых тегов {{ form }}
– единственный минус Django Forms. Схематичность разметки связана с тем, что рендеринг формы по умолчанию выполняется с помощью самых базовых HTML-тегов:
<div> <label for="id_first_name">Имя:</label> <input type="text" name="first_name" maxlength="20" required id="id_first_name"> </div> <div> <label for="id_last_name">Фамилия:</label> <input type="text" name="last_name" maxlength="20" required id="id_last_name"> </div> <div> <label for="id_photo">Фото:</label> <input type="file" name="photo" accept="image/*" required id="id_photo"> </div> <div> <label for="id_telegram">Телеграм:</label> <input type="text" name="telegram" maxlength="20" required id="id_telegram"> </div> <div> <label for="id_city">Город:</label> <input type="text" name="city" maxlength="20" required id="id_city"> </div> <div> <label for="id_profession">Профессия:</label> <input type="text" name="profession" maxlength="50" required id="id_profession"> </div> <div> <label for="id_bio">О себе:</label> <textarea name="bio" cols="40" rows="10" required id="id_bio"> </textarea> </div>
В итоге автоматически выведенная форма будет полностью рабочей и функциональной, но не слишком привлекательной. Этот недостаток, однако, очень легко исправить.
Как изменить дизайн Django форм
Придать форме более привлекательный внешний вид можно несколькими способами:
- Автоматически – с помощью дополнительного пакета типа django-crispy-forms, который передает в формы стили Bootstrap или Tailwind. Это самый простой, но не самый гибкий метод, к тому же у новых версий django-crispy-forms бывают конфликты с новыми релизами Django.
- С помощью уже упомянутого выше кастомного рендеринга
{{ form.name_of_field }}
, когда поля формы выводятся в шаблоне одно за другим и к ним можно применять любые HTML-теги и CSS-стили. - С использованием метода
field.widget.attrs.update()
в forms.py, который передает нужные CSS-стили в шаблон внутри формы.
Другой вариант – сделать нужный HTML/CSS дизайн для формы непосредственно в шаблоне. Чтобы Django мог обработать такую форму, в нее надо передать токен {% csrf_token %}
, как и в случае рендеринга встроенных форм Django, а если форм на странице несколько – обязательно указать нужные представления в form action:
<div class="col-md-5 col-xl-5 pe-xxl-0"> <div class="card card-bg hero-header-form"> <div class="card-body p-4 p-xl-6"> <h2 class="text-100 text-center">Вход</h2> <form action="{% url 'landingLogin' %}" method="POST" class="mb-3"> {% csrf_token %} <div class="form-floating mb-3"> <input class="form-control input-box form-ensurance-header-control" id="floatingName" type="text" name="username" placeholder="name"/> <label for="floatingName">Логин</label> </div> <div class="form-floating mb-3"> <input class="form-control input-box form-ensurance-header-control" id="floatingPassword" type="password" name="password" placeholder="••••••••"/> <label for="floatingPassword">Пароль</label> </div> <div class="col-12 d-grid"> <button class="btn btn-primary rounded-pill" type="submit">Войти</button> </div> </form> </div> </div> </div>
С помощью любого из этих способов можно привести форму в более презентабельный вид.
Mетод field.widget.attrs.update()
С помощью этого метода можно передать любые нужные CSS-стили (кастомные или стандартные Bootstrap'овские) прямо в форму. Добавим Bootstrap-стили в шаблон profile.html:
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet"> <title>Профиль пользователя</title> </head> <body> <nav class="navbar navbar-expand-lg bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="#">Компания</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav ms-auto me-5"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">Главная</a> </li> <li class="nav-item"> <a class="nav-link" href="#">О нас</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Контакты</a> </li> </ul> </div> </div> </nav> <div class="container"> <div class="row"> <div class="col-md-4 offset-md-4"> <h4 class="text-center text-muted">Заполните форму своими данными:</h4> <form method="post"> {% csrf_token %} {{ form.as_p }} <button type="submit" class="btn btn-primary btn-lg w-100">Сохранить</button> </form> </div> </div> </div> <footer class="bg-light text-center text-lg-start mt-5"> <div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.05);"> © 2022 Компания </div> </footer> </body> </html>
И воспользуемся методом field.widget.attrs.update() для передачи стилей form-control и form-label в форму:
from django import forms from .models import Profile class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = '__all__' labels = { 'first_name': 'Имя', 'last_name': 'Фамилия', 'photo': 'Фото', 'telegram': 'Телеграм', 'city': 'Город', 'profession': 'Профессия', 'bio': 'О себе', } def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) for name, field in self.fields.items(): field.widget.attrs.update({'class': 'form-control form-label'})
Результат:
Кастомный рендеринг форм
Вывод полей формы с помощью тега {{ form.name_of_field }} позволяет встроить Django-форму в более сложный дизайн – когда у каждого поля свой набор стилей.
Предположим, что наша форма должна выглядеть так:
Метод field.widget.attrs.update() в этом случае – не самое оптимальное решение, так у этой формы не только поля, но и оборачивающие эти поля div'ы имеют разные стили:
<form> <div class="row g-3"> <div class="col-md-6"> <label for="firstName" class="form-label">Имя</label> <input type="text" class="form-control" id="firstName"> </div> <div class="col-md-6"> <label for="lastName" class="form-label">Фамилия</label> <input type="text" class="form-control" id="lastName"> </div> </div> <div class="mt-3 mb-3"> <label for="photo" class="form-label">Фото профиля</label> <input class="form-control" type="file" id="photo"> </div> <div class="input-group mb-3"> <span class="input-group-text">@</span> <input type="text" class="form-control" placeholder="Телеграм" id="telegram"> </div> <div class="row g-3"> <div class="col-md-6"> <label for="city" class="form-label">Город</label> <input type="text" class="form-control" id="city"> </div> <div class="col-md-6"> <label for="profession" class="form-label">Профессия</label> <input type="text" class="form-control" id="profession"> </div> </div> <div class="mt-3 mb-3"> <label for="about" class="form-label">О себе</label> <textarea class="form-control" placeholder="Расскажите о себе" id="about" rows="4"></textarea> </div> <button type="submit" class="btn btn-primary btn-lg w-100">Сохранить</button> </form>
Лучшее решение – комбинированное использование field.widget.attrs.update() для передачи стилей form-control
в forms.py и {{ form.name_of_field }} в шаблоне profile.html для вывода полей формы по отдельности:
from django import forms from .models import Profile class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = '__all__' labels = { 'first_name': 'Имя', 'last_name': 'Фамилия', 'photo': 'Фото', 'telegram': 'Телеграм', 'city': 'Город', 'profession': 'Профессия', 'bio': 'О себе', } def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) for name, field in self.fields.items(): field.widget.attrs.update({'class': 'form-control'})
<form method="post"> <div class="row g-3"> <div class="col-md-6"> <label for="{{ form.first_name.auto_id }}" class="form-label">{{ form.first_name.label }}</label> {{ form.first_name }} </div> <div class="col-md-6"> <label for="{{ form.last_name.auto_id }}" class="form-label">{{ form.last_name.label }}</label> {{ form.last_name }} </div> </div> <div class="mt-3 mb-3"> <label for="{{ form.photo.auto_id }}" class="form-label">{{ form.photo.label }}</label> {{ form.photo }} </div> <div class="input-group mb-3"> <span class="input-group-text">@ник в Телеграме</span> {{ form.telegram }} </div> <div class="row g-3"> <div class="col-md-6"> <label for="{{ form.city.auto_id }}" class="form-label">{{ form.city.label }}</label> {{ form.city }} </div> <div class="col-md-6"> <label for="{{ form.profession.auto_id }}" class="form-label">{{ form.profession.label }}</label> {{ form.profession }} </div> </div> <div class="mt-3 mb-3"> <label for="{{ form.bio.auto_id }}" class="form-label">{{ form.bio.label }}</label> {{ form.bio }} </div> <button type="submit" class="btn btn-primary btn-lg w-100 mt-3 mb-5">Сохранить</button> </form>
Сохранение данных из формы
Итак, у нас есть определенная выше модель Profile, форма ProfileForm, и шаблоны для работы с ними:
Для сохранения данных из формы в модели напишем представление ProfileFormView, а для вывода сообщения об успешном сохранении данных – представление SuccessView:
from django.views.generic.edit import FormView from django.views.generic import TemplateView from .forms import ProfileForm from django.urls import reverse_lazy from django.contrib import messages class SuccessView(TemplateView): template_name = 'success.html' class ProfileFormView(FormView): form_class = ProfileForm template_name = 'profile.html' success_url = reverse_lazy('success') def form_valid(self, form): profile = form.save() return super().form_valid(form) def form_invalid(self, form): for field, errors in form.errors.items(): for error in errors: messages.error(self.request, f'В поле {field} возникла ошибка: {error}') return super().form_invalid(form)
Эти представления приводятся в действие маршрутами profile/ и success/:
from django.urls import path from .views import ProfileFormView, SuccessView urlpatterns = [ path('profile/', ProfileFormView.as_view(), name='profile'), path('success/', SuccessView.as_view(), name='success'), ]
Если с данными в форме все в порядке, они будут сохранены в БД, а пользователь будет перенаправлен на success.html:
Если же пользователь заполнил форму неправильно, он увидит сообщение об ошибке в конкретном поле формы:
В верхней части формы в шаблоне profile.html определен вывод возможных ошибок:
<form method="post" enctype="multipart/form-data"> {% csrf_token %} {% if messages %} <ul class="list-unstyled"> {% for message in messages %} <li{% if message.tags %} class="text-danger"{% endif %}>{{ message }}</li> {% endfor %} </ul> {% endif %} <div class="row g-3"> <div class="col-md-6"> <label for="{{ form.first_name.auto_id }}" class="form-label">{{ form.first_name.label }}</label> {{ form.first_name }} </div> <div class="col-md-6"> <label for="{{ form.last_name.auto_id }}" class="form-label">{{ form.last_name.label }}</label> {{ form.last_name }} </div> </div> <div class="mt-3 mb-3"> <label for="{{ form.photo.auto_id }}" class="form-label">{{ form.photo.label }}</label> {{ form.photo }} </div> <div class="input-group mb-3"> <span class="input-group-text">@ник в Телеграме</span> {{ form.telegram }} </div> <div class="row g-3"> <div class="col-md-6"> <label for="{{ form.city.auto_id }}" class="form-label">{{ form.city.label }}</label> {{ form.city }} </div> <div class="col-md-6"> <label for="{{ form.profession.auto_id }}" class="form-label">{{ form.profession.label }}</label> {{ form.profession }} </div> </div> <div class="mt-3 mb-3"> <label for="{{ form.bio.auto_id }}" class="form-label">{{ form.bio.label }}</label> {{ form.bio }} </div> <button type="submit" class="btn btn-primary btn-lg w-100 mt-3 mb-5">Сохранить</button> </form>
Кастомная валидация
Для каждого поля формы можно определить специальный валидатор значения. Django предоставляет несколько встроенных валидаторов, которые могут, к примеру, проверить:
- принадлежит ли число к нужному диапазону;
- не превышает ли текст максимальную длину;
- соответствует ли загружаемый файл допустимому формату;
- является ли выбранный юзернейм уникальным и т.д.
Если встроенных валидаторов недостаточно для решения задачи, всегда можно написать свой собственный. Сделаем, например, кастомный валидатор для проверки корректности юзернейма в Телеграме.
Валидация ника в Телеграме
Имя пользователя должно начинаться с @, может содержать символ _ (но не сразу после @), и должно состоять только из латинских букв и цифр. Учтем все эти условия при составлении регулярного выражения:
regex = r'^@[a-zA-Z0-9]+(_?[a-zA-Z0-9]+)*$'
Валидатор, использующий это выражение, располагается в forms.py:
from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ import re def validate_telegram(value): regex = r'^@[a-zA-Z0-9]+(_?[a-zA-Z0-9]+)*$' if not re.match(regex, value): raise ValidationError( _("Поле Telegram должно начинаться с '@', содержать только латинские буквы и цифры, может содержать символ '_', но не сразу после '@'."), params={'value': value}, )
И добавляется в поле telegram
:
class ProfileForm(forms.ModelForm): telegram = forms.CharField( validators=[validate_telegram] )
Теперь пользователь при вводе ника в некорректном формате получит сообщение об ошибке:
Подведем итоги
В этой главе мы научились создавать, рендерить, валидировать формы и сохранять данные из них в БД. Исходный код можно взять здесь.
Функциональность Django Forms сложно охватить в одной статье – в следующем проекте рассмотрим, как защищать формы с помощью капчи и как отправлять данные на имейл.