Блог

Дропшиппинг интернет-магазин на Django (часть 4)

4. Регистрации и авторизации пользователей на сайте

Внимание! Если у вас возникли проблемы с выполнением предыдущего этапа, то вы сможете зайти в соответствующий урок, скачать архив предыдущего этапа, инсталлировать его, и начать этот урок именно с того самого места, где закончился предыдущий!

Содержание курса:

Регистрация и авторизация в Django

Немного теории

Как уже неоднократно упоминалось, формат этого курса предполагает в первую очередь практику и ещё раз практику. И всё же, без определения понятий сессия, cookies, GET- и POST-запросы будет очень сложно понять суть того, что мы будет делать дальше. Поэтому, несколько слов об этом, всё же, стоит сказать.

Понятие сессии, cookies

Как сайте “узнает”, что на него заходят “свои”, а не “чужие”?

Давайте откроем в браузере вкладку Storage → Cookies и попробуем зарегистрироваться на сайте. Мы увидим, что в момент регистрации появится новый ключ session_id, который будет существовать ровно до тех пор, пока мы авторизованы на этом сайте. Если мы откроем базу данных этого сайта, то в таблице в таблице django_session увидим тот же самый сессионный ключ.

Однако, если мы разлогинимся, то этот session_id исчезнет и из базы, и из cookies браузера. Дело в том, что после авторизации пользователя на сайте, сервер создаёт в своей БД особую цифровую метку, копию который он отправляет браузеру пользователя, и браузер сохраняет эту цифровую метку в своей памяти. Это и есть ключ сессии, который сохраняется в памяти браузера. Эти данные, сохранённые в памяти браузера и называются cookies.

(На всякий случай: браузер для SQLite, упомянутый в видео, можно установить с помощью этих команд:
$ sudo add-apt-repository -y ppa:linuxgndu/sqlitebrowser
$ sudo apt-get update
$ sudo apt-get install sqlitebrowser
)

До тех пор, пока пользователь залогинен на сайте, session_id в cookies его браузера совпадает с session_id, сохраненными в БД. Что даёт возможность пользователю посещать любые страницы сайте, к которым у него есть доступ.

Для этого браузер при каждом новом запросе на сервер отправляет туда session_id, и пока этот код совпадает с кодом в БД сервера, пользователь остаётся авторизованным.

Если же пользователь разлогинится, то сервер удалит его session_id из своей базы данных. И на те страницы сайта, где требуется авторизованный вход, этот пользователь войти уже на сможет.

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

Модель User

Все таблицы в базе данных (БД) Django описываются с помощью моделей. Это специальные Python классы, наследуемые от класса Model.

Одна модель описывает одну таблицу БД. А каждая строка этой модели описывает одно поле таблицы.

Более подробно мы разберём примеры моделей на следующем 5-м уроке. Ну, а пока просто примем к сведению, что модель пользователя User, имеющая определённый набор базовых полей и методов, уже существуют по умолчанию. Она всегда создаётся в Django во время первой миграции. В частности, там уже есть поля username, email, password, is_staff (является ли пользователь сотрудником) и is_superuser.

POST и GET запросы

Новые незнакомые аббревиатуры обычно внушают страх. Чтобы избавиться от, как минимум, половины этого страха, сразу же отметим, что все запросы, о которых мы говорили до сих пор - это GET запросы. Правда, ведь, совсем не страшно?!! Тогда идём дальше!

Итак, при обращению к серверу возможны разные типы запросов. Вообще, HTTP протокол имеет их достаточно много, но в данном курсе мы будем использовать только два:

  • GET-запрос - когда нам нужно просто отобразить какую-либо страницу
  • и POST-запрос - когда нам требуется передать на сервер данные для их сохранения в БД

Для передачи GET-запроса достаточно указать обычный адрес url. А вот передача POST запроса ОБЯЗАТЕЛЬНО предполагает наличие формы и csrf-токена.

Формы, csrf-token

Форма в HTML - это специальная конструкция, которая заключена в тег <form></form>. Внутри этого могут также содержаться поля ввода с одиночным тегом <input>, которые обычно идут в паре с тегом <label></label>, который поясняет, что именно надо вводить в поле input.

Форма также содержит кнопку (поле ввода) с type="submit", нажатие на которую как раз и является командой отправки данных на сервер.

Примерно ток выглядит форма в коде HTML:

<form method="post" action="/your-url/">
    <label for="username">User name: </label>
    <input id="username" type="text" name="username" value="{{ username }}">
    <input type="submit" value="OK">
</form>

Передача url в другие приложения. Страницы register.html и login.html

В предыдущем уроке мы уже научились передавать управление из конфигуратора main/urls.py во view. Однако, хорошей практикой является и другой вариант: промежуточная передача url из главного конфигуратора в конфигуратор другого приложения.

Для этого в main/urls.py мы добавим строку, благодаря который все запросы авторизации, начинающиеся с ‘auth/’ будут передаваться на обработку в другой конфигуратор - authentication/urls.py:

from django.contrib import admin
from django.urls import path, include

from company import views as views_company

urlpatterns = [
    path('', views_company.home_page, name='index'),
    path('about/', admin.site.urls),
    path('admin/', admin.site.urls),
    path('auth/', include('authentication.urls')),
]

Следовательно, в authentication/urls.py появятся запросы, соответствующие авторизации, регистрации и разлогиниванию:

from django.urls import path

from authentication import views

urlpatterns = [
    path('login/', views.login_user, name='login'),
    path('register/', views.register, name='register'),
    path('logout/', views.logout_user, name='logout'),
]

Теперь, для начала сделаем простые view, которые будут запускаться по этим трём запросам в приложении authentication:

def login_user(request):
    return render(request, ‘auth/login.html’)

def register(request):
    return render(request, ‘auth/register.html’)

def logout_user(request):
    pass

C разлогиниванием пока ничего не понятно - страницы под него не предусмотрено, поэтому вместо кода просто напишем pass.

Ну а, вообщето, шаблон здесь и не нужен. А нужна специальная функция разлогинивания logout(request)

Тем не менее, если мы ограничимся во view только этой функцией, то получим ошибку: The view authentication.views.logout_user didn't return an HttpResponse object. It returned None instead.

Поэтому в конце view нам придётся вернуть переход на какую-либо страницу. Например на главную. Сделать это можно с помощью функции redirect('index'), где 'index' - имя адреса в authentication/urls.

Создание формы в модуле form.py

Очевидно, что теперь конструкция для всех view в приложении авторизации будет не такой простой, как была для index view. Поскольку, теперь у нас появилась новая задача: прочитать данные с HTML-страницы, передать их на сервер и далее в таблицу User нашей базы данных . И, как уже говорилось выше, всё, что передёются в базу данных, должно передаваться только с помощью формы и POST-запроса.

Для создания форм в Django предусмотрен специальный модуль forms.py, в котором можно предварительно создать объект "форма ввода" и подробно описать его свойства и характеристики. Форма обычно связана с конкретной таблицей в базе данных. Поэтому, создавая форму следует:

  • Указать к какой таблице эта форма относится
  • Выбрать список полей (можно выбрать все поля и указать конкретный список)
  • Определить список обязательных к заполнению полей
  • Описать прочие свойства полей
  • и так далее

Поскольку мы используем уже готовую Django’вскую таблицу User с довольно-таки объёмным списком стандартных полей, то в нашей форме обязательно следует указать какие именно поля будут использованы:

from django import forms
from django.contrib.auth.models import User


class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput())

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

В следующем уроке мы увидим, что создание формы внешне очень похоже на создание таблицы базы данных.

Получение данных с html-страницы

Таким образом, в наше вью login_user необходимо будет добавить следующие опции:

  • Передать форму LoginForm на страницу login.html
  • Обеспечить безопасный ввод данных в эту форму
  • Добавить на страницу кнопку управления, с помощью который пользователь сможет передать данные на сервер
  • Обеспечить приём данных на сервере, их валидацию
  • И, если это необходимо, добавить полученные данные в соответствующую таблицу базы данных

Передать любые данные на страницу можно с помощью дополнительно созданного словаря context. В нашем варианте по ключу login_form мы добавим экземпляр класса только что созданной формы LoginForm():

def login_user(request):
    context = {'login_form': LoginForm()}
    return render(request, 'auth/login.html', context)

Для передачи данных на html-страницу этого вполне достаточно, но для получения данных со страницы потребуется добавить обработку данных по POST-запросу.

def login_user(request):
    context = {'login_form': LoginForm()}

    if request.method == 'POST':
        login_form = LoginForm(request.POST)
        if login_form.is_valid():
            username = login_form.cleaned_data['username']
            password = login_form.cleaned_data['password']
            user = authenticate(username=username, password=password)
            if user:
                login(request, user)
                return redirect('index')

    return render(request, 'auth/login.html', context)

Одновременно с этим на html-странице необходимо будет добавить:

  • Теги формы <form></form>
  • В теге формы следует обязательно указать метод POST и url, по которому этот POST-запрос должен быть обработан
  • Добавить тег <input> с параметром name, равным имени полей формы
  • И, наконец, добавить тег <input> (или <button>) с параметром type=’”submit” по клику на который пользователь сможет передать данные на сервер.

То есть фрагмент формы в HTML-коде будет выглядеть примерно так:

<form method="post" action="{% url 'login' %}">
    {% csrf_token %}
    <label class="form-label" for="id_username">Login</label>
    <input type="text" class="form-input" id="id_username"
                               name="username" placeholder="Login" required>
    <label class="form-label" for="id_password">Password</label>
    <input type="password" class="form-input" id="id_password"
                               name="password" placeholder="Password" required>
    <button type="submit" class="request-quote-btn">Log In</button>
</form>

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

Обработка ошибок ввода данных

Заполнение формы может быть с ошибками - это совершенно нормально. Не нормально, если сервер “молча” не примет эти данные и не “объяснит” пользователю почему.

Есть, как минимум, два способа.

1.) Самое простое решение - просто сообщить пользователю, что введённые данные не верны. Для этого в словарь context просто добавляется новое поле attention. Кроме того, правильно будет вернуть форму именно с теми данными, которые не прошли.

if user:
    login(request, user)
    return redirect('index')
context = {
    'login_form': LoginForm(request.POST),
    'attention': f'The user with name {username} is not registered in the system!'}

2.) Другой, более изящный и аккуратный способ: сообщить пользователю какие именно ошибки возникли в процессе валидации. И здесь потребуется изменения непосредственно в самой форме:

def clean(self):
    cleaned_data = super().clean()
    username = cleaned_data.get('username')
    password = cleaned_data.get('password')

    try:
        self.user = User.objects.get(username=username)
    except User.DoesNotExist:
        raise forms.ValidationError(f'User with username: [{username}] does not exist!')

    if not self.user.check_password(password):
        raise forms.ValidationError('Could not log in using these email and password')

    return cleaned_data

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

{% for error in login_form.non_field_errors %}
    <div class="alert alert-danger">{{ error }}</div>
{% endfor %}

На этом миссию создания страницы авторизации можно считать выполненной!

Создание view регистрации с помощью generic views

Помимо уже хорошо известных нам вью-функций, есть также вью-классы, наследуемые от generic views. Более того, последним отдаётся предпочтение при решении сложных задач, поскольку generic views выглядят более компактно и лучше читаются.

Наше знакомство с темой generic views мы начнём пока только с одного варианта - класса TemplateView. (Далее в этом курсе мы рассмотрим ещё несколько видов generic views).

В качестве примера перепишем наше register вью, выполненное на основе функции, в RegisterView с использованием класса TemplateView:

from django.views.generic import TemplateView

class RegisterView(TemplateView):
    template_name = 'auth/register.html'

Для обработки GET и POST запросов у этого класса есть методы get и post:

def get(self, request):
        user_form = RegisterForm()
        context = {'user_form': user_form}
        return render(request, 'auth/register.html', context)

    def post(self, request):
        user_form = RegisterForm(request.POST)
        if user_form.is_valid():
            user = user_form.save()
            user.set_password(user.password)
            user.save()
            login(request, user)
            return redirect('index')

        context = {'user_form': user_form}
        return render(request, 'auth/register.html', context)

На способ создания записи нового пользователя в таблице User стоит обратить особое внимание, так как этот способ мы будем применять и далее:

  • Сначала создаётся объект user - экземпляр модели (класса) User (но иногда это можно сделать с помощью метода save() класса ModelForm).
  • Далее (если необходимо) этот объект может изменяться / дополняться (в примере выше мы сохраняем строковое значение пароля в формате хэш-суммы).
  • Но непосредственно в базу данных новый объект добавляется с помощью другого метода save() - метода Django-модели.

О форме RegisterForm, которая используется здесь в методе post, следует сказать отдельно.

Формы класса ModelForm

Предыдущая форма LoginForm была создана на основе класса Form - довольно-таки простого варианта, внешне очень похожего на класс моделей Model.

Формы, созданные с помощью класса ModelForm, являются более “продвинутым” вариантом. Здесь нам уже нет надобности прописывать каждое поле формы, - достаточно просто указать имя модели, которая будет использоваться в этой форме, и список необходимых полей. После этого, формат полей из привязанной к форме модели автоматически перенесётся в саму форму.

Пример новой формы:

class RegisterForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('username', 'email', 'password')

Встраивание формы в HTML-код

В предыдущем примере с формой авторизации LoginForm мы предприняли попытку встроить готовую форму в HTML-страницу с помощью тега {{ login_form }}. Однако, быстро отказались от этой затеи - уж больно сильно отличались стили стандартной Django-формы и стили вёрстки нашей страницы login.html.

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

  • Вывод непосредственно самого поля ввода.
  • Связанные с этим полем метки (заголовка) поля.
  • Ошибки ввода, относящиеся к данному полю.

Пример HTML-кода, реализующие описанное выше:

{% for field in user_form %}
<div class="row">
    <div class="col-12 col-lg-4 col-md-4 offset-md-4">
        <div class="quote-form-wrapper">
            <span style="color: red">{{ field.errors }}</span>
            {{ field.label_tag}}
            {{ field }}
        </div>
    </div>
</div>
{% endfor %}

Более подробно с со всеми деталями этого этапа вы сможете познакомиться из этого видео:






Переход на следующий этап проекта