Зарегистрироваться на Yatube теперь может каждый! Но не каждый захочет: страница работает, но выглядит непрезентабельно:
Посмотрим, что можно сделать.
Шаблон страницы регистрации сейчас выглядит так:
<!-- templates/users/signup.html -->
{% extends "base.html" %}
{% block title %}Зарегистрироваться{% endblock %}
{% block content %}
<form method="post" action="{% url 'users:signup' %}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Зарегистрироваться">
</form>
{% endblock %}
Переменная form
передаётся в шаблон, на её основе генерируется HTML-код формы. Метод as_p()
выводит код формы, обрамляя каждую строку формы в HTML-тег <p>
. А в начале кода формы стоит странный тег шаблонизатора {% csrf_token %}
Что такое {% csrf_token %} ?
CSRF (от англ. cross-site request forgery — межсайтовая подделка запроса) — вид атаки на сайт или на аккаунт пользователя на сайте, где пользователь авторизован. Логика подобной атаки довольно проста:
- предположим, пользователь залогинен на сайте своего банка;
- пользователь заходит на некую стороннюю веб-страницу и нажимает на заражённую ссылку;
- при клике отправляется запрос на сайт банка (и так совпало, что это именно банк нашего пользователя: если банк популярен — вероятность такого совпадения велика);
- браузер помнит данные пользователя, и поэтому отправленный запрос будет воспринят сайтом банка как запрос авторизованного пользователя;
- посредством такого запроса злоумышленники могут вывести деньги с аккаунта пользователя или сделать заказ от его имени;
- профит!
Конечно, в этом сценарии должен присутствовать элемент невезения: запрос должен отправляться на сайт именно того банка, где залогинен пользователь. Но если банк крупный, а число пользователей, кликающих на хакерскую ссылку, велико — такая схема сработает.
Для защиты от таких атак сайты генерируют специальный csrf-токен (или csrf-ключ), который встраивается в «доверенную» веб-страницу и отправляется вместе с каждым запросом от пользователя к серверу. Этот ключ — уникальная для каждого пользователя строка, последовательность символов. Угадать её практически невозможно.
При получении запроса сервер сравнивает ключ, полученный с запросом, с образцом, сохранённым на сервере, и обрабатывает запрос только в случае совпадения.
Django серьёзно относится к безопасности, и все формы с POST-запросами по умолчанию должны быть защищены таким ключом.
Работа с формами
Возьмём для экспериментов несложную форму.В ней есть два текстовых поля — «Имя» и «Фамилия», и кнопка «Отправить». При нажатии на кнопку браузер отправит данные из формы на страницу, указанную в атрибуте action.
<!-- Форма оборачивается тегом form,
в открывающем теге указывается тип и адрес запроса для отправки данных -->
<form method="post" action="/example-action/">
<!-- Тег label - заголовок для поля ввода -->
<label>Введите имя:</label>
<!-- type - тип поля ввода (здесь - текстовое поле),
name - имя поля в POST-запросе, в котором будет отправлена информация,
введённая пользователем -->
<input type="text" name="first_name" id="id_first_name">
<label for="id_last_name">Введите фамилию:</label>
<input type="text" name="last_name" id="id_last_name">
<!-- Этот код отрисует кнопку с надписью "Отправить" -->
<input type="submit" value="Отправить">
</form>
Устройство форм в Django
Чтобы было проще понимать примеры и код, разберём устройство форм на высоком уровне.Форма в Django не обязательно должна работать с моделью: можно создать форму, не связанную с моделью. Поля формы и их типы описывают при создании класса формы; такие классы наследуют от встроенного класса Form.
from django import forms
class ContactForm(forms.Form):
first_name = forms.CharField(label='Введите имя', initial='Лев')
last_name = forms.CharField(label='Введите фамилию', initial='Толстой')
Форма может содержать поля разных типов, все они описаны в документации. Когда в шаблоне поле превращается в HTML-код, то применяется виджет, определённый параметром widget
. Виджет — это шаблон, по которому генерируется HTML-код поля формы.
Основные типы полей, которые вам будут встречаться:
- BooleanField — соответствует типу bool. Виджет по умолчанию отрисовывает чекбокс
<input type="checkbox">
; - CharField — поле для ввода текста, по умолчанию используется виджет однострочного поля ввода
<input type="text">
. Виджет можно заменить: если указать в параметрахwidget=forms.Textarea
, будет отрисовано поле многострочного ввода,<textarea>
; - ChoiceField — поле выбора из выпадающего списка,
<select>
; - EmailField — однострочное поле ввода текста, но с обязательной проверкой введённой строки на соответствие формату email;
- FileField — поле для отправки файла, в шаблоне отрисует тег
<input type="file">
. Есть аналогичное поле для отправки только файлов изображений: ImageField; - IntegerField — поле для ввода чисел:
<input type="number">
.
Можно самостоятельно создавать новые типы полей и новые виджеты. В Django есть множество готовых виджетов: например, есть виджет для превращения поля ввода в визуальный редактор.
Работа с формами из кода
Запустите в консоли интерактивный режим Django, дальше работать будем в нём: (venv) ... $ python manage.py shell
Импортируем модуль forms
и создадим класс формы с двумя полями:
>>> from django import forms
>>> class Registration(forms.Form):
... first_name = forms.CharField(label='Введите имя', initial='Лев')
... last_name = forms.CharField(label='Введите фамилию', initial='Толстой')
...
>>> form = Registration()
>>> #напечатаем результат, чтобы увидеть HTML-код, который выведет метод as_p()
>>> print(form.as_p())
<p><label for="id_first_name">Введите имя:</label> <input type="text" name="first_name" value="Лев" required id="id_first_name"></p>
<p><label for="id_last_name">Введите фамилию:</label> <input type="text" name="last_name" value="Толстой" required id="id_last_name"></p>
Сгенерированный HTML-код содержит код полей ввода <input type="text" ...>
с необходимыми атрибутами и теги <label>
— заголовки полей, видимые пользователям.
Метод as_p()
, унаследованный от класса Form, обрамляет каждую пару тегов «label + поле» в HTML-тег <p>
По умолчанию форма выводится в HTML-таблицу, в элемент <table>
. Такой же код будет выведен и методом as_table()
:
>>> print(form.as_table())
<tr><th><label for="id_first_name">Введите имя:</label></th><td><input type="text" name="first_name" value="Лев" required id="id_first_name"></td></tr>
<tr><th><label for="id_last_name">Введите фамилию:</label></th><td><input type="text" name="last_name" value="Толстой" required id="id_last_name"></td></tr>
Поля можно вывести HTML-списком, методом as_ul()
:
>>> print(form.as_ul())
<li><label for="id_first_name">Введите имя:</label> <input type="text" name="first_name" value="Лев" required id="id_first_name"></li>
<li><label for="id_last_name">Введите фамилию:</label> <input type="text" name="last_name" value="Толстой" required id="id_last_name"></li>
Каждый из этих методов можно вызывать из шаблона командами form.as_table , form.as_p или form.as_ul.
Когда форма заполнена и отправлена, Django получит данные и проверит их на корректность. В случае, если отправленная информация не прошла валидацию, то объект form
получит список ошибок в атрибуте form.errors .
Работа с полями формы
С объектом формы можно работать через цикл for
:
>>> for field in form:
... print(field)
...
# поля формы будут напечатаны по очереди
<input type="text" name="first_name" value="Лев" required id="id_first_name">
<input type="text" name="last_name" value="Толстой" required id="id_last_name">
В шаблоне этот же код выглядит так:
{% for field in form %}
{{ field }}
{% endfor %}
Доступ к полям формы по именам
Иногда удобно вывести в шаблон поля формы не циклом, а отдельным кодом.В шаблоне для доступа к полю применяют точечную нотацию: form.field_name
.
<form method="post">
{% csrf_token %}
{{ form.first_name }}
{{ form.last_name }}
<input type="submit" value="Отправить">
</form>
В Python-коде доступ к полям можно получить, обратившись к объекту form
как к словарю, где ключом является имя поля.
>>> print(form['first_name'])
<input type="text" name="first_name" value="Лев" required id="id_first_name">
Атрибуты полей формы
При выводе формы в шаблон доступны атрибуты объекта field
:
- field.label — метка поля, параметр
label
из описания поля в классе:label="Введите имя"
; - field.label_tag — этот атрибут даёт доступ к полному тегу
label
для поля:<label for="id_first_name">Введите имя:</label>
; - field.id_for_label — здесь хранится значение, которое в HTML-теге
label
указывает, для какого именно поля формы создан этотlabel
. В примере<label for="id_first_name">Введите имя:</label>
значением тега field.id_for_label будет id_first_name; - field.value — значение, которое ввёл пользователь;
- field.html_name — атрибут name тега
input
; - field.help_text — текст подсказки, который можно передать в коде;
- field.errors — этот атрибут будет заполнен, если при проверке отправленных данных произошла ошибка.
Создание нового шаблона
В документации по Bootstrap даётся такой пример HTML-кода формы:
<form>
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input
type="email"
class="form-control"
id="exampleInputEmail1"
aria-describedby="emailHelp"
placeholder="Enter email"
>
<small id="emailHelp" class="form-text text-muted">
We'll never share your email with anyone else.
</small>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input
type="password"
class="form-control"
id="exampleInputPassword1"
placeholder="Password"
>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="exampleCheck1">
<label class="form-check-label" for="exampleCheck1">Check me out</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Измените код шаблона templates/users/signup.html
так, чтобы он соответствовал стандарту Bootstrap.
{% extends "base.html" %}
{% block title %}Зарегистрироваться{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8 p-5">
<div class="card">
<div class="card-header">
Зарегистрироваться
</div>
<div class="card-body">
{# Этот блок будет показан, если в форме есть ошибки #}
{% if form.errors %}
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger">
{{ error|escape }}
</div>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger">
{{ error|escape }}
</div>
{% endfor %}
{% endif %}
<form method="post" action="{% url 'users:signup' %}">
{% csrf_token %}
{% for field in form %}
<div class="form-group row my-3 p-3">
<label for="{{ field.id_for_label }}">
{{ field.label }}
{% if field.field.required %}
<span class="required text-danger">*</span>
{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<small id="{{ field.id_for_label }}-help" class="form-text text-muted">
{{ field.help_text|safe }}
</small>
{% endif %}
</div>
{% endfor %}
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
Зарегистрироваться
</button>
</div>
</form>
</div> <!-- card body -->
</div> <!-- card -->
</div> <!-- col -->
</div> <!-- row -->
{% endblock %}
Форма регистрации теперь выглядит гораздо лучше: