В этой части будем реализовывать механизм авторизации пользователей через страницы логина и регистрации на Django 1.11 c валидацией форм и возможностью logout. В статье приведен код для python3.

Создание проекта

Для начала создадим проект myproject через консоль в нужной нам папке и перейдем в папку проекта. Внутри проекта создадим приложение джанго - registration

1
2
3
$ django-admin startproject myproject
$ cd myproject
$ python manage.py startapp registration

теперь структура внутри registration выглядит следующим образом

1
2
3
4
5
6
7
8
│ admin.py
│ apps.py
│ models.py
│ tests.py
│ views.py
│ __init__.py
└───migrations

Так как в Django реализована архитектура MVC, то в нашем случае “M” - models.py, это файл, где прописывается логика взаимодействия с базой данных, “C”, в случае Django “Template”, это представление данных в браузере через html, а views это бизнес-логика, как применять модели и шаблоны. Годная картинка по теме:

MTV

Так как авторизация в нашем случае для тестового проекта, а не для большого e-commerce сайта, то реализуем взаимодействие с базой данных не через models.py, а через форму и встроенную в Django модель User, так что файл models.py можем удалять, как и tests.py - в этой части мы их оставим. Теперь структура нашего приложения выглядит так:

1
2
3
4
5
6
7
│ admin.py
│ apps.py
│ forms.py
│ views.py
│ __init__.py
└───migrations

Всего у нас будет три страницы html: для тех, кто пришел в первый раз(login.html), для регистрации(register.html) и для тех, кто успешно прошел регистрацию и зашел через логин(success.html).

Итого у нас два use-case

  • для пришедших в первый раз: login -> register -> login -> success
  • для пришедших повторно login -> success

Обрабатывать результаты будем через формы и регистрационные данные записывать в базу данных sqlite3(которая создается по умолчанию для проекта Django). Перейдем к коду.

Находим наши страницы

Для примера возьмем бутстраповские страницы логина и регистрации и немного их изменим, дополнив тегами Jinja2.

Все темплейты хранятся в папке templates, которую мы создадим в основном проекте.Таким образом, структура проекта со всеми добавленными темплейтами будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
|-- myproject
| `-- __pycache__
|-- registration
| |-- __pycache__
| `-- migrations
| `-- __pycache__
|-- templates
| -- base.html
| -- login.html
| -- register.html
` -- success.html

Код страницы логина. При сабмите формы идет ссылка на функцию account_login c post запросом. {% csrf_token %} это обеспечение безопасности от межсайтовой подделки запроса. Используется в формах в связи с post-запросами. Они опаснее обычных get запросов, т.к. взаимодействуют с базой данных.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container" style="margin-top:40px">
<div class="row">
<div class="col-sm-6 col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<strong> Sign in to continue</strong>
</div>
<div class="panel-body">
{% if error %}
<p class="text-danger">Please enter a correct username and password.
Note that both fields may be case-sensitive.</p>
{% endif %}
<form role="form" action="{% url "account_login" %}" method="POST">{% csrf_token %}
<fieldset>
<div class="row">
<div class="center-block">
<img class="profile-img"
src="http://bit.ly/2gSVBOy"
alt="" style="width: 96px; height: 96px; margin: 0 auto 10px;
display: block; -moz-border-radius: 50%; -webkit-border-radius: 50%;
border-radius: 50%;">
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-10 col-md-offset-1 ">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-user"></i>
</span>
<input class="form-control" placeholder="Username"
name="username" type="text" autofocus>
</div>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input class="form-control" placeholder="Password" name="password"
type="password" value="">
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-lg btn-primary btn-block">Sign in
</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
<div class="panel-footer" style="padding: 1px 15px;
color: #A0A0A0">
Don't have an account? <a href="{% url "account_register" %}"> Sign Up Here </a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Как выглядит в браузере

login

Код страницы регистрации с валидацией ошибок. При сабмите формы также идет ссылка на функцию account_register c post запросом.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{% extends "base.html" %} {% block title %}Register{% endblock %}
{% block content %}
<div class="container" style="margin-top: 60px">
<div class="row centered-form">
<div class="col-xs-12 col-sm-8 col-md-4 col-sm-offset-2
col-md-offset-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Register</h3>
</div>
<div class="panel-body">
<form role="form" method="POST" action="{% url "
account_register" %}">{% csrf_token %}
<div class="form-group{% if errors.username %}
has-error{% endif %}">
<input type="username" name="username"
id="username" class="form-control input-sm"
placeholder="Username">
<span class="text-danger">
{{ errors.username|striptags }}</span>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-6">
<div
class="form-group{% if errors.first_name %} has-error{% endif %}">
<input type="text" name="first_name"
id="first_name"
class="form-control input-sm"
placeholder="First Name">
<span class="text-danger">
{{ errors.first_name|striptags }}</span>
</div>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<div
class="form-group{% if errors.last_name %} has-error{% endif %}">
<input type="text" name="last_name"
id="last_name"
class="form-control input-sm"
placeholder="Last Name">
<span class="text-danger">
{{ errors.last_name|striptags }}</span></div>
</div>
</div>
<div class="form-group{% if errors.email %} has-error{% endif %}">
<input type="email" name="email" id="email"
class="form-control input-sm" placeholder="Email Address">
<span class="text-danger">{{ errors.email|striptags }}</span>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-6">
<div
class="form-group{% if errors.password %} has-error{% endif %}">
<input type="password" name="password" id="password"
class="form-control input-sm" placeholder="Password">
<span class="text-danger">{{ errors.password|striptags }}
</span></div>
</div>
<div class="col-xs-6 col-sm-6 col-md-6">
<div
class="form-group{% if errors.password %} has-error{% endif %}">
<input type="password" name="confirm_password"
id="confirm_password" class="form-control input-sm"
placeholder="Confirm Password">
</div>
</div>
</div>
<button type="submit" class="btn btn-info btn-block">Register</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Как выглядит страница регистрации

register

И страницу успешного входа мы увидим уже в конце, если у нас все получится=)

В начале каждого документа стоит тег {% extends "base.html" %}, что является расширением одного темплейта на базе основного, в Джанго основной темплейт классически называется base.html. Это удобно, так как не приходится копировать один и тот же код в хедере разных страниц. Получается, что весь контент отдельной страницы помещается между тегами {% block content %} и {% endblock %} . Подробнее можно почитать здесь.

Содержание base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}
{% endblock %} | Auth app</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>

Пишем вьюжки

Для обработки страниц в файле views.py пишем три функции на каждую из страниц (login,register,success) и одну функцию logout. Импортируем нужные нам модули Django, из интересных штук тут есть декоратор login_required, который “дополняет” функцию успешного входа - success(), проверяя прошел ли предварительно логин в системе.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User
from django.contrib.auth import login, logout
from django.template.response import TemplateResponse
from django.http import HttpResponseRedirect
from django.urls import reverse
from .forms import UserCreateForm
def login(request):
""" Функция, которая принимает http-запрос, если произведен get-запрос,
возвращает страницу логина. Если post-запрос, берет данные из базы данных,
и проверяет логин и пароль с помощью встроенной формы AuthenticationForm,
при успешном сценарии вызывает метод login, который принимает объект
реквеста и юзера, сохраняет id usera в сессию Django
и возвращает страницу success """
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.user_cache)
return HttpResponseRedirect(reverse('account_success'))
else:
return TemplateResponse(request, 'login.html', {'error': True})
else:
return TemplateResponse(request, 'login.html', {})
@login_required
def success(request):
""" Функция, которая принимает http-запрос get и возвращает страницу
success, если пользователь успешно залогинен."""
return TemplateResponse(request, 'success.html', {})
@login_required
def account_logout(request):
""" Функция, которая принимает http-запрос и разлогинивает пользователя. """
logout(request)
return HttpResponseRedirect(reverse('account_login'))
def account_register(request):
""" Функция, которая принимает http запрос, если произведен get-запрос,
возвращает страницу регистрации. Если post-запрос, проверяет форму
UserCreateForm на валидность, при успешном завершении заносит данные в
таблицу auth_user в базу. При успешном сценарии возвращает обратно на
страницу логина """
form = UserCreateForm()
if request.method == 'POST':
form = UserCreateForm(data=request.POST)
if form.is_valid():
user = User(
username=form.cleaned_data.get('username'),
email=form.cleaned_data.get('email'),
first_name=form.cleaned_data.get('first_name'),
last_name=form.cleaned_data.get('last_name')
)
user.set_password(form.cleaned_data.get('password'))
user.save()
return HttpResponseRedirect(reverse('account_login'))
return TemplateResponse(request, 'register.html', {'errors': form.errors})
else:
return TemplateResponse(request, 'register.html', {"form": form})

Разбираемся с формами

В функции register мы ссылаемся на форму UserCreateForm, которая находится в forms.py. В этом файле мы прописываем нужные нам поля из страницы регистрации. Метод clean() выполняет роль проверки совпадения значений, которые пользователь указывает в “введите пароль” и “повторно введите пароль”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from django.forms import Form, CharField, EmailField
from django.forms.utils import ErrorList
class RegistrationForm(Form):
username = CharField(required=True)
email = EmailField(required=True)
password = CharField(required=True)
confirm_password = CharField(required=True)
first_name = CharField(required=False)
last_name = CharField(required=False)
def clean(self):
cleaned_data = super(RegistrationForm, self).clean()
if cleaned_data.get('password') != cleaned_data.get('confirm_password'):
self._errors['password'] = ErrorList('The passwords you entered do not match')
return cleaned_data

Конфигурация URL-ов

Теперь самое время настроить ссылки, по которым мы будем обращаться к нашим функциям во views. Для этого в файле urls.py внутри папки приложения registration пропишем следующее:

1
2
3
4
5
6
7
8
9
10
from django.conf.urls import url
from registration import views
urlpatterns = [
url(r'^login/', views.account_login, name='account_login'),
url(r'^success/', views.account_success, name='account_success'),
url(r'^register/', views.account_register, name='account_register'),
url(r'^logout/', views.account_logout, name='account_logout'),
]

Таким образом, мы обозначаем, что если пользователь перешел по ссылке адресприложения/account/login, то вызывается функция views.login, если другой запрос - остальные ссылки по аналогии. Для конфигурации ссылок в Джанго используются регулярные выражения, тема обширная, думаю, для отдельной статьи. Пока что просто воспримем как есть.

Затем включим эти url в наше основное приложение - myproject, чтобы обращаться к приложению registration. Для этого в папке myproject в файле urls.py исправим следующее

1
2
3
4
5
6
7
8
from django.conf.urls import url, include # добавляем метод include
from django.contrib import admin # это прописывается автоматически, не меняем
urlpatterns = [
url(r'^admin/', admin.site.urls), # также не меняем
url(r'^account/', include('registration.urls')) # добавляем urls нашего приложения registration
]

Теперь мы должны получить страницу логина при входе. Таким образом, ссылки как бы нанизываются друг на друга. account + login в url передаст в браузер нужную нам страницу логина. Попробуем. Для корректной работы формы сделаем миграцию базы данных и запустим проект.

1
2
3
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

Результаты

gif

Ну чтож, даже ребята со стоковых фото считают, что мы молодцы. Уж они явно что-то знают.

Успешно

Код этой статьи можно найти в моем репозитории на github.