Использование форм, не связанных с моделями

Курс по Django: https://stepik.org/a/183363

Архив проекта: lesson-13-coolsite.zip

На данный момент мы с вами, в целом, познакомились с механизмом маршрутизации, построения моделей, работы с шаблонами и использованием админ-панели. Конечно, это далеко не полный функционал фреймворка Django и мы еще будем возвращаться к этим темам, но на этом занятии мы коснемся новой темы – работы с формами.

Те из вас, кто уже создавал свои сайты, знают, что формы – это один из важнейших элементов большинства сайтов. Например, когда мы выполняем авторизацию или регистрацию, то появляется страница с полями ввода, чекбоксами, кнопками, списками и другими элементами интерфейса:

Это и есть формы. В HTML они задаются тегом <form> и служат для передачи на сервер пользовательской информации, например, логина и пароля для входа на сайт. На этом занятии мы увидим, как реализуются формы во фреймворке Django. Подробную документацию об этом вы можете почитать на русскоязычном сайте:

https://djbook.ru/rel3.0/index.html

в разделе «Формы», или на официальном англоязычном сайте:

https://www.djangoproject.com

Первое, что нужно знать, это то, что формы в Django можно создавать в связке с моделью какой-либо таблицы БД. Тогда получаем формы, связанные с моделью. Например, когда мы выполняем авторизацию или регистрацию на сайте, то этот процесс, очевидно, связан с данными таблиц, тогда используются формы, связанные с моделями.

Но можно создавать и независимые формы, не привязанные к моделям. Например, когда создается простой поиск или идет отправка письма на электронную почту. Если при этом обращение к БД не требуется, то и форма создается как независимая, самостоятельная.

Сначала мы рассмотрим форму, не связанную с моделью, хотя и сделаем это на примере добавления статей в БД. На следующем занятии модифицируем ее и превратим в форму, связанной с моделью.

В главном меню у нас уже есть пункт для добавления статей:

http://127.0.0.1:8000/addpage/

и функция представления addpage (в файле women/views.py). Немного изменим эту функцию так, чтобы она отображала шаблон addpage.html:

def addpage(request):
    return render(request, 'women/addpage.html', {'menu': menu, 'title': 'Добавление статьи'})

А сам шаблон addpage.html определим так:

{% extends 'women/base.html' %}
 
{% block content %}
<h1>{{title}}</h1>
<p>Содержимое страницы
{% endblock %}

Теперь, при обновлении увидим полноценную страницу для добавления нового поста.

Создание класса формы

Все готово для создания формы. В Django существует специальный класс Form, на базе которого удобно создавать формы, не связанные с моделями. Где следует объявлять формы? Обычно, для этого создают в приложении отдельный файл forms.py. Мы так и сделаем (создаем файл women/forms.py). И в этом файле импортируем пакет forms и наши модели (модель Category нам здесь понадобится для формирования списка категорий):

from django import forms
from .models import *

Следующий шаг – объявить класс AddPostForm, описывающий форму добавления статьи. Он будет унаследован от базового класса Form и иметь следующий вид:

class AddPostForm(forms.Form):
    title = forms.CharField(max_length=255)
    slug = forms.SlugField(max_length=255)
    content = forms.CharField(widget=forms.Textarea(attrs={'cols': 60, 'rows': 10}))
    is_published = forms.BooleanField()
    cat = forms.ModelChoiceField(queryset=Category.objects.all())

Смотрите, мы здесь определяем только те поля, с которыми будет взаимодействовать пользователь. Например, поля модели time_create или time_update нигде не фигурируют, так как заполняются автоматически. Далее, каждый атрибут формы лучше назвать также, как называются поля в таблице women. Впоследствии нам это облегчит написание кода.

В классе формы каждый атрибут – это ссылка на тот или иной экзнмпляр класса из пакета forms. Например, title определен через класс CharField, поле is_published – через BooleanField, а список категорий cat – через класс ModelChoiceField, который формируется из данных таблицы Category.

Почему указаны именно такие классы? И какие классы вообще существуют для формирования полей формы? Полный их список и назначение можно посмотреть на странице русскоязычной документации:

https://djbook.ru/rel3.0/ref/forms/fields.html

Я советую вам в целом изучить его и знать, как создавать различные типы полей. В частности, класс CharField служит для создания обычного поля ввода, класс BooleanField – для checkbox’а, класс ModelChoiceField – списка с данными из указанной модели.

Использование формы в функции представления

После того, как форма определена, ее можно использовать в функции представления addpage. В самом простом варианте можно записать так:

def addpage(request):
    form = AddPostForm()
    return render(request, 'women/addpage.html', {'menu': menu, 'title': 'Добавление статьи', 'form': form})

Здесь создается экземпляр формы и через переменную form передается шаблону addpage.html. Но это будет работать только при первом отображении формы, когда пользователь еще не заполнил ее поля, то есть, когда форма не ассоциирована с данными. При повторном ее отображении, например, если данные были введены некорректно и нужно показать ошибки ввода, то форма должна сохранять ранее введенную пользователем информацию. Чтобы проделать такой трюк, в функции представления пропишем следующее условие:

def addpage(request):
    if request.method == 'POST':
        form = AddPostForm(request.POST)
        if form.is_valid():
            print(form.cleaned_data)
    else:
        form = AddPostForm()
 
    return render(request, 'women/addpage.html', {'menu': menu, 'title': 'Добавление статьи', 'form': form})

Мы здесь вначале проверяем, если пришел POST-запрос, значит, пользователем были отправлены данные (мы будем передавать их именно POST-запросом). В этом случае наполняем форму принятыми значениями из объекта request.POST и, затем, делаем проверку на корректность заполнения полей (метод is_valid). Если проверка прошла, то в консоли отобразим словарь form.cleaned_data полученных данных от пользователя. Если же проверка на пройдет, то пользователь увидит сообщения об ошибках. Ну, а если форма показывается первый раз (идем по else), то она формируется без параметров и отображается с пустыми полями.

Отображение формы в шаблоне

Осталось отобразить форму в нашем шаблоне. Перейдем в файл addpage.html и пропишем там следующие строчки:

<form action="{% url 'add_page' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Добавить</button>
</form>

Смотрите, мы самостоятельно прописываем тег <form> для создания формы в HTML-документе, указываем через атрибут action ее обработчик (в данном случае – это тот же адрес страницы, и связанная с ним функция представления addpage). Атрибут method указывает способ передачи информации на сервер (используется POST-запрос). Внутри формы записываем специальный тег csrf_token, который генерирует скрытое поле с уникальным токеном. Это необходимо для защиты от CSRF-атак, когда на каком-либо другом сайте злоумышленник создает по виду неотличимую форму от вашего сайта и пытается заставить пользователя отправить актуальные данные на сервер через подложную форму. Фреймворк Django не станет обрабатывать данные, если отсутствует или не совпадает токен csrf-поля и, тем самым, защищает пользователя от подобных атак.

Следующая строчка {{ form.as_p }} вызывает метод as_p нашей формы для отображения ее полей, используя теги абзацев <p>. Существуют и другие методы, которые формируют поля в виде элементов списка <ul> или в виде таблицы. Последний вариант, хоть и возможен, но считается устаревшей практикой. Здесь также стоит иметь в виду, что по умолчанию все поля в Django обязательны, если не указано обратное через параметр required=False.

Наконец, последняя строчка – тег <button> создает кнопку типа submit для запуска процесса отправки данных формы на сервер и, в конечном итоге, нашей функции представления addpage.

Если теперь обновить страницу, то увидим все указанные поля формы со списком и кнопкой.

Улучшение внешнего вида формы

Но у нас названия полей отображаются по-английски и нам бы хотелось их изменить. Для этого у каждого класса полей формы есть специальный атрибут label, который и позволяет задавать свои имена, например, так:

class AddPostForm(forms.Form):
    title = forms.CharField(max_length=255, label="Заголовок")
    slug = forms.SlugField(max_length=255, label="URL")
    content = forms.CharField(widget=forms.Textarea(attrs={'cols': 60, 'rows': 10}), label="Контент")
    is_published = forms.BooleanField(label="Публикация")
    cat = forms.ModelChoiceField(queryset=Category.objects.all(), label="Категории")

Теперь, все выглядит гораздо приятнее. Давайте для примера сделаем поле content необязательным, а поле is_published с установленной галочкой. Соответственно, в классе CharField пропишем параметр required=False, а в классе BooleanField – параметр initial=True. Еще в классе ModelChoiceField добавим параметр empty_label="Категория не выбрана", чтобы вместо черточек отображалась по умолчанию в списке эта фраза.

Со всеми возможными параметрами можно ознакомиться в документации, все по той же ссылке:

https://djbook.ru/rel3.0/ref/forms/fields.html

Способы отображения формы в шаблонах

Давайте теперь посмотрим, что же в действительности представляет собой объект form на примере ручного перебора и отображения всех наших полей. Я сейчас уберу строчку {{ form.as_p }} и вместо нее запишу все поля формы по порядку, друг за другом.

Первое поле title мы сформируем так:

<p><label class="form-label" for="{{ form.title.id_for_label }}">{{form.title.label}}: </label>{{ form.title }}</p>
<div class="form-error">{{ form.title.errors }}</div>

Смотрите, мы здесь самостоятельно прописали HTML-теги внутри формы. Сначала идет тег абзаца <p>, внутри него тег <label> для оформления надписи. У нее указан класс оформления form-label и идентификатор через свойство form.title.id_for_label. Далее, идет само название form.title.label и после тега <label> отображается поле для ввода заголовка form.title. Вот так можно самостоятельно расписать атрибуты объекта form внутри шаблона. Ну а следующая строчка определяет тег <p> с классом оформления form-error для отображения возможных ошибок при вводе неверных данных. Список ошибок доступен через переменную form.title.errors.

Все, если теперь обновить страницу сайта, то увидим это одно поле в форме. По аналогии можно прописать и все остальные поля:

<p><label class="form-label" for="{{ form.slug.id_for_label }}">{{form.slug.label}}: </label>{{ form.slug }}</p>
<div class="form-error">{{ form.slug.errors }}</div>
<p><label class="form-label" for="{{ form.content.id_for_label }}">{{form.content.label}}: </label>{{ form.content }}</p>
<div class="form-error">{{ form.content.errors }}</div>
<p><label class="form-label" for="{{ form.is_published.id_for_label }}">{{form.is_published.label}}: </label>{{ form.is_published }}</p>
<div class="form-error">{{ form.is_published.errors }}</div> 
<p><label class="form-label" for="{{ form.cat.id_for_label }}">{{form.cat.label}}: </label>{{ form.cat }}</p>
<div class="form-error">{{ form.cat.errors }}</div>

А в самом верху добавим строчку:

<div class="form-error">{{ form.non_field_errors }}</div>

для вывода ошибок валидации, не связанных с заполнением того или иного поля.

Это довольно гибкий вариант представления формы и здесь можно очень тонко настроить отображение каждого поля. Однако, объем кода при этом резко возрастает. В большинстве случаев все это можно существенно сократить. Вы, наверное, уже заметили, что все эти строчки, в общем-то, повторяются, а значит, их можно сформировать через цикл, следующим образом:

<div class="form-error">{{ form.non_field_errors }}</div>
 
{% for f in form %}
<p><label class="form-label" for="{{ f.id_for_label }}">{{f.label}}: </label>{{ f }}</p>
<div class="form-error">{{ f.errors }}</div>
{% endfor %}

Я, думаю, этот ход вполне понятен: мы здесь на каждой итерации имеем объект поля формы и также обращаемся к нужным его атрибутам. Такая запись значительно короче и гибче в плане изменения формы (достаточно поменять класс формы и это автоматом изменит ее вид в шаблоне). Правда, все поля теперь будут иметь одни и те же стили оформления.

Однако, для виджетов стили оформлений можно прописать непосредственно в классе формы. Например, у класса поля ввода title добавить именованный параметр widget:

title = forms.CharField(max_length=255, label="Заголовок", widget=forms.TextInput(attrs={'class': 'form-input'}))

Мы здесь формируем виджет через класс TextInput и указываем у него стиль оформления form-input. При обновлении страницы, видим, что первое поле изменило свой вид. И так можно делать со всеми полями.

Тестирование формы

Если мы попробуем отправить пустую форму на сервер, то браузер укажет, что поле title обязательное. То же самое и для поля URL, напишем что-нибудь латинскими буквами. Выберем категорию и нажмем «Добавить». В результате, в консоли у нас отображается словарь с принятыми данными:

{'title': 'Ариана Гранде', 'slug': 'gfhjghjghjg', 'content': '', 'is_published': True, 'cat': <Category: Певицы>}

Все это мы можем сохранить в БД и сформировать новый пост. Если же данные в форме окажутся некорректными, например, в поле URL напишем что-то русскими буквами и сделаем отправку. Видим сообщение:

«Значение должно состоять только из латинских букв…»

и в консоли нет отображения данных, то есть, проверка не прошла. Вот так автоматически Django позволяет выполнять проверки на валидность заполненных данных.

Добавление новой записи

Теперь, когда наша форма в целом готова, выполним добавление записи в БД. Для этого после проверки валидности данных, запишем конструкцию:

        if form.is_valid():
            #print(form.cleaned_data)
            try:
                Women.objects.create(**form.cleaned_data)
                return redirect('home')
            except:
                form.add_error(None, 'Ошибка добавления поста')

Мы здесь используем ORM Django для формирования новой записи в таблице women и передаем методу create распакованный словарь полученных данных. Так как метод create может генерировать исключения, то помещаем его вызов в блок try и при успешном выполнении, осуществляется перенаправление на главную страницу. Если же возникли какие-либо ошибки, то попадаем в блок except и формируем общую ошибку для ее отображения в форме.

Давайте, для начала введем корректные данные в форму, тогда после нажатия на кнопку «Добавить» в таблице women появится новая запись с пустым полем для изображения и заполненными всеми остальными полями. Вернемся в форму и попробуем добавить статью с неуникальным слагом. Тогда возникнет исключение и мы увидим сообщение «Ошибка добавления поста». Как видите, все достаточно просто.

Это был пример использования формы не связанной с моделью. В результате, нам пришлось в классе AddPostForm дублировать поля, описанные в модели Women и, кроме того, вручную выполнять сохранение данных в таблицу women. На следующем занятии мы увидим, как все это можно автоматизировать, используя форму в связке с моделью.

Курс по Django: https://stepik.org/a/183363

Видео по теме