Что является самым главным для веб-приложения? Возможность для любого пользователя, живущего в любой стране мира, создать учетную запись в вашем приложении и начать работать с ним. В этой главе мы создадим формы, в которых пользователи смогут вводить свои темы и записи, а также редактировать существующие данные. Кроме того, вы узнаете, как Django защищает приложения от распространенных атак на страницы с формами, чтобы вам не приходилось тратить много времени на продумывание средств защиты вашего приложения.
Затем будет реализована система проверки пользователей. Мы создадим страницу регистрации, на которой пользователи смогут создавать учетные записи, и ограничим доступ к некоторым страницам для анонимных пользователей. Затем некоторые функции представления будут изменены так, чтобы пользователь мог видеть только собственные данные. Вы узнаете, как обеспечить безопасность и конфиденциальность данных пользователей.
Прежде чем создавать систему аутентификации пользователей, позволяющую заводить учетные записи, сначала добавим несколько страниц, на которых пользователи смогут вводить собственные данные. У них появится возможность создавать новые темы, добавлять новые записи и редактировать сделанные ранее.
В настоящее время данные может вводить только суперпользователь на административном сайте. Однако разрешать пользователям работать на нем явно нежелательно, поэтому мы воспользуемся средствами создания форм Django для создания страниц, на которых пользователи смогут вводить данные.
Начнем с возможности создания новых тем. Страницы на базе форм добавляются практически так же, как и те страницы, которые мы уже создали ранее: вы определяете URL, пишете функцию представления и создаете шаблон. Принципиальное отличие — добавление нового модуля forms.py, содержащего функциональность форм.
Любая страница, на которой пользователь может вводить и отправлять информацию, является формой, даже если на первый взгляд она на форму не похожа. Когда пользователь вводит информацию, необходимо проверить (validate), что он ввел корректные данные, а не вредоносный код (например, нарушающий работу сервера). Затем проверенная информация обрабатывается и сохраняется в нужном месте базы данных. Django автоматизирует бо́льшую часть этой работы.
Простейший способ создания форм в Django основан на использовании класса ModelForm, который автоматически создает форму на основании моделей, определенных в главе 18. Ваша первая форма будет создана в файле forms.py, который должен находиться в одном каталоге с models.py:
forms.py
from django import forms
from .models import Topic
❶ class TopicForm(forms.ModelForm):
class Meta:
❷ model = Topic
❸ fields = ['text']
❹ labels = {'text': ''}
Сначала импортируется модуль forms и модель, с которой мы будем работать: Topic. В строке ❶ определяется класс TopicForm, наследующий от forms.ModelForm.
Простейшая версия ModelForm состоит из вложенного класса Meta, который сообщает Django, на какой модели должна базироваться форма и какие поля должны на ней находиться. Форма создается на базе модели Topic ❷, а на ней размещается только поле text ❸. Благодаря пустой строке в словаре labels Django получает указание не генерировать подпись для текстового поля ❹.
URL новой страницы должен быть простым и содержательным, поэтому после того, как пользователь выбрал команду создания новой темы, его направляют по адресу http://localhost:8000/new_topic/. Ниже приведена схема URL для страницы new_topic, которая добавляется в файл learning_logs/urls.py:
learning_logs/urls.py
--пропуск--
urlpatterns = [
--пропуск--
# Страница для добавления новой темы.
path('new_topic/', views.new_topic, name='new_topic'),
]
Эта схема URL будет отправлять запросы функции представления new_topic(), которую мы сейчас напишем.
Функция new_topic() должна обрабатывать две разные ситуации: исходные запросы страницы new_topic (в этом случае должна отображаться пустая форма) и обработка данных, отправленных в форме. Затем функция должна перенаправить пользователя обратно на страницу topics:
views.py
from django.shortcuts import render, redirect
from .models import Topic
from .forms import TopicForm
--пропуск--
def new_topic(request):
"""Добавляет новую тему."""
❶ if request.method != 'POST':
# Данные не отправлялись; создается пустая форма.
❷ form = TopicForm()
else:
# Отправлены данные POST; обработать данные.
❸ form = TopicForm(data=request.POST)
❹ if form.is_valid():
❺ form.save()
❻ return redirect('learning_logs:topics')
# Вывести пустую или недействительную форму.
❼ context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
Мы импортируем класс HttpResponseRedirect, который будет использоваться для перенаправления пользователя к странице topics после отправки введенной темы. Функция reverse() определяет URL по заданной схеме URL (то есть Django сгенерирует URL при запросе страницы). Вдобавок импортируется только что написанная форма TopicForm.
При создании веб-приложений применяются два основных типа запросов: GET и POST. Запросы GET используются для страниц, которые только читают данные с сервера, а запросы POST обычно используются в тех случаях, когда пользователь должен отправить информацию в форме. Для обработки всех наших форм будет использоваться метод POST (существуют и другие разновидности запросов, но в нашем проекте они не используются).
Функция new_topic() получает в параметре объект запроса. Когда пользователь впервые запрашивает эту страницу, его браузер отправляет запрос GET. Когда пользователь уже заполнил и отправил форму, его браузер отправляет запрос POST. В зависимости от типа запроса мы определяем, запросил пользователь пустую форму (запрос GET) или предлагает обработать заполненную (запрос POST).
Метод запроса — GET или POST — проверяется условием if в строке ❶. Если метод отличается от POST, то, вероятно, используется запрос GET, поэтому необходимо вернуть пустую форму (даже если это запрос другого типа, это все равно безопасно). Мы создаем экземпляр TopicForm ❷, сохраняем его в переменной form и отправляем форму шаблону в словаре context ❼. При создании TopicForm аргументы не передавались, поэтому Django создает пустую форму, которая заполняется пользователем.
Если используется метод запроса POST, то выполняется блок else, который обрабатывает данные, отправленные в форме. Мы создаем экземпляр TopicForm ❸ и передаем ему данные, введенные пользователем, хранящиеся в request.POST. Возвращаемый объект form содержит информацию, отправленную пользователем.
Отправленную информацию нельзя сохранять в базе данных до тех пор, пока она не будет проверена ❹. Функция is_valid() проверяет, что все обязательные поля были заполнены (все поля формы по умолчанию являются обязательными), а введенные данные соответствуют типам полей — например, что длина текста меньше 200 символов, как было указано в файле models.py в главе 18. Автоматическая проверка избавляет нас от большого объема работы. Если все данные действительны, то можно вызвать метод save() ❺, который записывает данные из формы в базу данных.
После того как данные будут сохранены, страницу можно покинуть. Функция redirect() принимает имя представления и перенаправляет пользователя на связанную с ним страницу. Мы используем вызов redirect() для перенаправления браузера на страницу topics ❻, на которой пользователь увидит только что введенную им тему в общем списке тем.
Переменная context определяется в конце функции представления ❼, а страница создается на базе шаблона new_topic.html, который будет создан на следующем шаге. Код размещается за пределами любых блоков if; он выполняется при создании пустой формы, а также при определении того, что отправленная форма была недействительной. Недействительная форма содержит стандартные сообщения об ошибках, чтобы помочь пользователю передать действительные данные.
Теперь создадим новый шаблон new_topic.html для отображения только что созданной формы:
new_topic.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
❶ <form action="{% url 'learning_logs:new_topic' %}" method='post'>
❷ {% csrf_token %}
❸ {{ form.as_p }}
❹ <button name="submit">add topic</button>
</form>
{% endblock content %}
Этот шаблон расширяет base.html, поэтому имеет такую же базовую структуру, как и остальные страницы «Журнала обучения». Сначала определяется форма HTML ❶ с помощью тегов <form></form>. Аргумент action сообщает серверу, куда передавать данные, отправленные формой; в данном случае они возвращаются функции представления new_topic(). Аргумент method дает браузеру указание отправить данные в запросе типа POST.
Django использует шаблонный тег {% csrf_token %} ❷ для предотвращения попыток получения несанкционированного доступа к серверу (атаки такого рода называются межсайтовой подделкой запросов (cross-site request forgery)). Далее отображается форма; это наглядный пример того, насколько легко в Django выполняются такие стандартные операции, как отображение формы. Чтобы автоматически создать все необходимые для этой операции поля, достаточно добавить шаблонную переменную {{ form.as_p }} ❸. Модификатор as_p дает Django указание отобразить все элементы формы в формате абзацев (<div></div>) — это простой способ аккуратного отображения формы.
Django не создает кнопку отправки данных для форм, поэтому мы определяем ее, прежде чем закрыть форму ❹.
Далее ссылка на страницу new_topic создается на странице topics:
topics.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
--пропуск--
</ul>
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a>
{% endblock content %}
Поместите ссылку после списка существующих тем. Полученная форма изображена на рис. 19.1. Воспользуйтесь ею и добавьте несколько своих тем.
Рис. 19.1. Страница для добавления новой темы
Теперь, когда пользователь может добавлять новые темы, он захочет добавлять и новые записи. Мы снова определим URL, напишем новую функцию, шаблон и создадим ссылку на страницу. Но сначала нужно добавить в файл forms.py еще один класс.
Мы должны создать форму, связанную с моделью Entry, но более специализированную по сравнению с TopicForm:
forms.py
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
--пропуск--
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
❶ labels = {'text': ''}
❷ widgets = {'text': forms.Textarea(attrs={'cols': 80})}
Сначала в команду import к Topic добавляется Entry. Новый класс EntryForm наследует от forms.ModelForm и содержит вложенный класс Meta с указанием модели, на которой он базируется, и поле, добавляемое в форму. Полю 'text' снова назначается пустая надпись ❶.
Для класса EntryForm мы добавляем атрибут widgets ❷. Виджет (widget) представляет собой элемент формы HTML: однострочное или многострочное текстовое поле, раскрывающийся список и т.д. Добавляя атрибут widgets, вы можете переопределить виджеты, выбранные Django по умолчанию. Давая Django указание использовать элемент forms.Textarea, мы настраиваем виджет ввода для поля 'text', чтобы ширина текстовой области составляла 80 столбцов вместо значения по умолчанию 40. У пользователя будет достаточно места для создания содержательных записей.
Необходимо добавить аргумент topic_id в URL для создания новой записи, поскольку запись должна быть связана с конкретной темой. Вот как выглядит URL, который мы добавляем в файл learning_logs/urls.py:
learning_logs/urls.py
--пропуск--
urlpatterns = [
--пропуск--
# Страница для добавления новой записи.
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]
Эта схема URL соответствует любому URL в форме http://localhost:8000/new_entry/id/, где id — число, равное идентификатору темы. Код <int:topic_id> захватывает числовое значение и сохраняет его в переменной topic_id. При запросе URL, соответствующего этой схеме, Django передает запрос и идентификатор темы функции представления new_entry().
Данная функция очень похожа на функцию добавления новой темы. Добавьте в файл views.py следующий код:
views.py
from django.shortcuts import render, redirect
from .models import Topic
from .forms import TopicForm, EntryForm
--пропуск--
def new_entry(request, topic_id):
"""Добавляет новую запись по конкретной теме."""
❶ topic = Topic.objects.get(id=topic_id)
❷ if request.method != 'POST':
# Данные не отправлялись; создается пустая форма.
❸ form = EntryForm()
else:
# Отправлены данные POST; обработать данные.
❹ form = EntryForm(data=request.POST)
if form.is_valid():
❺ new_entry = form.save(commit=False)
❻ new_entry.topic = topic
new_entry.save()
❼ return redirect('learning_logs:topic', topic_id=topic_id)
# Вывести пустую или недействительную форму.
context = {'topic': topic, 'form': form}
return render(request, 'learning_logs/new_entry.html', context)
Мы обновляем команду import и добавляем в нее только что созданный класс EntryForm. Определение new_entry() содержит параметр topic_id, позволяющий сохранять полученное из URL значение. Идентификатор темы понадобится для отображения страницы и обработки данных формы, поэтому мы используем topic_id для получения правильного объекта темы ❶.
Далее проверяется метод запроса: POST или GET ❷. Блок if выполняется для запроса GET, и мы создаем пустой экземпляр EntryForm ❸.
Для метода запроса POST мы обрабатываем данные, создавая экземпляр EntryForm, заполненный данными POST из объекта request ❹. Затем проверяется, корректны ли данные формы. Если да, то необходимо задать атрибут topic объекта записи перед сохранением его в базе данных. При вызове save() мы добавляем аргумент commit=False ❺ для того, чтобы дать Django указание создать новый объект записи и сохранить его в new_entry, не включая пока в базу данных. Мы присваиваем атрибуту topic объекта new_entry тему, прочитанную из базы данных в начале функции ❻, после чего вызываем save() без аргументов. В результате запись сохраняется в базе данных с правильной связанной темой.
Вызов redirect() в строке ❼ получает два аргумента: имя представления, которому передается управление, и аргумент для функции представления. В данном случае происходит перенаправление функции topic(), которой должен передаваться аргумент topic_id. Вызов перенаправляет пользователя на страницу темы, для которой была создана запись, и пользователь видит новую запись в соответствующем списке.
В конце функции создается словарь context, а страница создается на базе шаблона new_entry.html. Этот код выполняется для пустой или отправленной формы, которая была определена как недействительная.
Как видно из следующего кода, шаблон new_entry похож на шаблон new_topic:
new_entry.html
{% extends "learning_logs/base.html" %}
{% block content %}
❶ <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Add a new entry:</p>
❷ <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name='submit'>add entry</button>
</form>
{% endblock content %}
В начале страницы выводится тема ❶, чтобы пользователь мог видеть, в какую тему добавляется новая запись. Вдобавок тема служит ссылкой для возврата к основной странице этой темы.
Аргумент action формы содержит значение topic_id из URL, чтобы функция представления могла связать новую запись с правильной темой ❷. В остальном этот шаблон почти не отличается от new_topic.html.
Затем необходимо создать ссылку на страницу new_entry на каждой странице темы:
topic.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topic: {{ topic }}</p>
<p>Entries:</p>
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
</p>
<ul>
--пропуск--
</ul>
{% endblock content %}
Ссылка добавляется перед выводом записей, поскольку добавление новой записи является самым частым действием на этой странице. На рис. 19.2 изображена страница new_entry. Теперь пользователь может добавить сколько угодно новых тем и записей по каждой из них. Опробуйте страницу new_entry, добавив несколько записей для каждой из созданных вами тем.
Рис. 19.2. Страница new_entry
А теперь мы создадим страницу, на которой пользователи смогут редактировать ранее добавленные записи.
В URL страницы должен передаваться идентификатор редактируемой записи. Для этого в файл learning_logs/urls.py вносятся следующие изменения:
urls.py
--пропуск--
urlpatterns = [
--пропуск--
# Страница для редактирования записи.
path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]
Эта схема URL соответствует URL типа http://localhost:8000/edit_entry/id/. В примере значение id присваивается параметру entry_id. Django отправляет запросы, соответствующие этому формату, функции представления edit_entry().
Когда страница edit_entry получает запрос GET, edit_entry() возвращает форму для редактирования записи. При получении запроса POST с отредактированной записью страница сохраняет измененный текст в базе данных:
views.py
from django.shortcuts import render, redirect
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
--пропуск--
def edit_entry(request, entry_id):
"""Редактирует существующую запись."""
❶ entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
# Исходный запрос; форма заполняется данными текущей записи.
❷ form = EntryForm(instance=entry)
else:
# Отправка данных POST; обработать данные.
❸ form = EntryForm(instance=entry, data=request.POST)
if form.is_valid():
❹ form.save()
❺ return redirect('learning_logs:topic', topic_id=topic.id)
context = {'entry': entry, 'topic': topic, 'form': form}
return render(request, 'learning_logs/edit_entry.html', context)
Сначала необходимо импортировать модель Entry. Мы получаем объект записи, который пользователь хочет изменить ❶, и связанную с ней тему. В блоке if, который выполняется для запроса GET, создается экземпляр EntryForm с аргументом instance=entry ❷. Благодаря этому аргументу Django получает указание создать форму, заранее заполненную информацией из существующего объекта записи. Пользователь видит имеющиеся данные и может отредактировать их.
При обработке запроса POST передаются аргументы instance=entry и data=request.POST ❸. Они сообщают Django, что нужно создать экземпляр формы на основании информации существующего объекта записи, обновленный данными из request.POST. Затем проверяется, корректны ли данные формы. Если да, то следует вызов save() без аргументов ❹. Далее происходит перенаправление на страницу темы ❺, и пользователь видит обновленную версию отредактированной им записи.
Если отображается исходная форма для редактирования записи или отправленная форма недействительна, то создается словарь context, а страница создается на базе шаблона edit_entry.html.
Шаблон edit_entry.html очень похож на new_entry.html:
edit_entry.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Edit entry:</p>
❶ <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
{% csrf_token %}
{{ form.as_div }}
❷ <button name="submit">save changes</button>
</form>
{% endblock content %}
Аргумент action отправляет форму функции edit_entry() для обработки ❶. Идентификатор записи добавляется как аргумент в тег {% url %} , чтобы функция представления могла изменить правильный объект записи. Кнопка отправки данных создается с текстом, который напоминает пользователю, что он сохраняет изменения, а не создает новую запись ❷.
Теперь необходимо добавить ссылку на страницу edit_entry в каждую тему на странице со списком тем:
topic.html
--пропуск--
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}</p>
<p>
<a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
</p>
</li>
--пропуск--
После даты и текста каждой записи добавляется ссылка редактирования. Мы используем шаблонный тег {% url %} для определения схемы URL из именованной схемы edit_entry и идентификатора текущей записи в цикле (entry.id). Текст ссылки "edit entry" выводится после каждой записи на странице. На рис. 19.3 показано, как выглядит страница со списком тем с этими ссылками.
Рис. 19.3. Каждая запись снабжается ссылкой, позволяющей редактировать эту запись
Приложение «Журнал обучения» уже сейчас содержит бо́льшую часть необходимой функциональности. Пользователи могут добавлять темы и записи, а также читать любые записи по своему усмотрению. В следующем разделе мы реализуем систему регистрации пользователей, чтобы любой желающий мог создать свою учетную запись в «Журнале обучения» и добавить собственный набор тем и записей.
Упражнения
19.1. Блог. Создайте новый проект Django Blog. Создайте приложение blogs с двумя моделями: одна представляет весь блог, а вторая — отдельную публикацию в блоге. Создайте для каждой модели соответствующий набор полей. Создайте суперпользователя для проекта и с помощью административного сайта напишите пару коротких сообщений. Создайте главную страницу, на которой все сообщения будут выводиться в хронологическом порядке.
Создайте формы для создания блога и новых сообщений, а также для редактирования существующих сообщений. Заполните формы и убедитесь, что они работают.
В этом разделе мы создадим систему регистрации и авторизации пользователей, чтобы люди могли создать учетную запись, начать и завершать сеанс работы с приложением. Для всей функциональности, относящейся к работе с пользователями, будет создано отдельное приложение. Мы также слегка изменим модель Topic, чтобы каждая тема была связана с конкретным пользователем.
Начнем с создания нового приложения accounts с помощью команды startapp:
(ll_env)learning_log$ python manage.py startapp accounts
(ll_env)learning_log$ ls
❶ accounts db.sqlite3 learning_logs ll_env ll_project manage.py
(ll_env)learning_log$ ls accounts
❷ __init__.py admin.py apps.py migrations models.py tests.py views.py
Система аутентификации по умолчанию создается вокруг концепции учетных записей пользователей, поэтому имя accounts упрощает интеграцию с системой по умолчанию. Команда startapp, показанная выше, создает каталог accounts ❶ со структурой, идентичной приложению learning_logs ❷.
Новое приложение необходимо добавить в INSTALLED_APPS файла settings.py:
settings.py
--пропуск--
INSTALLED_APPS = [
# Мои приложения
'learning_logs',
'accounts',
# Приложения Django по умолчанию.
--пропуск--
]
--пропуск--
Django добавляет приложение accounts в общий проект.
Затем необходимо изменить корневой файл urls.py, чтобы он содержал URL, написанные для приложения accounts:
ll_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('', include('learning_logs.urls')),
]
Вставим строку для добавления файла urls.py из accounts. Эта строка будет соответствовать любому URL, начинающемуся со слова accounts, — например, http://localhost:8000/accounts/login/.
Начнем с реализации страницы входа. Мы воспользуемся стандартным представлением login, которое предоставляет Django, так что шаблон URL выглядит немного иначе. Создайте новый файл urls.py в каталоге ll_project/accounts/ и добавьте в него следующий код:
accounts/urls.py
"""Определяет схемы URL для пользователей."""
from django.urls import path, include
app_name = 'accounts'
urlpatterns = [
# Добавить URL авторизации по умолчанию.
path('', include('django.contrib.auth.urls')),
]
Сначала импортируется функция path, а затем функция include, позволяющая добавить аутентификационные URL по умолчанию, определенные Django. Эти URL по умолчанию содержат именованные схемы, такие как 'login' и 'logout'. Переменной app_name присваивается значение 'accounts', чтобы инфраструктура Django могла отличить эти URL от URL, принадлежащих другим приложениям. Даже URL по умолчанию, предоставляемые Djano, при добавлении в файл urls.py приложения accounts будут доступны через пространство имен accounts.
Схема страницы входа соответствует URL http://localhost:8000/accounts/login/. Когда Django читает этот URL, слово accounts указывает, что следует обратиться к accounts/urls.py, а login — что запросы должны отправляться представлению login по умолчанию.
Когда пользователь запрашивает страницу входа, Django использует свое представление login по умолчанию, но мы все равно должны предоставить шаблон для этой страницы. Аутентификационные представления по умолчанию ищут шаблоны в каталоге registration, поэтому вы должны создать его. В каталоге ll_project/accounts/ создайте каталог templates, а внутри него — еще один каталог registration. Вот как выглядит шаблон login.html, который должен находиться в ll_project/accounts/templates/registration/:
login.html
{% extends "learning_logs/base.html" %}
{% block content %}
❶ {% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
❷ <form action="{% url 'accounts:login' %}" method='post'>
{% csrf_token %}
❸ {{ form.as_p }}
❹ <button name="submit">log in</button>
</form>
{% endblock content %}
Шаблон расширяет base.html, чтобы страница входа по оформлению и поведению была похожа на другие страницы сайта. Обратите внимание: шаблон в одном приложении может расширять шаблон из другого приложения.
Если у формы установлен атрибут errors, то выводится сообщение об ошибке ❶. В нем говорится, что комбинация имени пользователя и пароля не соответствует информации, хранящейся в базе данных.
Мы хотим, чтобы представление обработало форму, поэтому аргументу action присваивается URL страницы входа ❷. Представление отправляет объект form шаблону, мы должны вывести форму ❸ и добавить кнопку отправки данных ❹.
После успешной авторизации пользователя в системе Django необходимо перенаправить его на нужную страницу. Мы можем настроить это поведение в файле настроек.
Добавьте в конец файла settings.py следующий код:
settings.py
--пропуск--
# Мои настройки.
LOGIN_REDIRECT_URL = 'learning_logs:index'
Поскольку в файле settings.py указаны все настройки по умолчанию, полезно выделить раздел, в котором мы будем добавлять новые. Первой мы добавим настройку LOGIN_REDIRECT_URL, которая сообщает Django, по какому URL перенаправлять пользователя после успешной авторизации.
Добавим ссылку на страницу входа в base.html, чтобы она имелась на каждой странице. Ссылка не должна отображаться, если пользователь уже прошел процедуру входа, поэтому мы вкладываем ее в тег {% if %}:
base.html
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a> -
❶ {% if user.is_authenticated %}
❷ Hello, {{ user.username }}.
{% else %}
❸ <a href="{% url 'accounts:login' %}">Log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
В системе аутентификации Django в каждом шаблоне доступна переменная user, которая всегда содержит атрибут is_authenticated: он равен True, если пользователь прошел проверку, и False в противном случае. Это позволяет вам выводить разные сообщения для проверенных и непроверенных пользователей.
В данном случае мы выводим приветствие для пользователей, выполнивших вход ❶. Для проверенных пользователей устанавливается дополнительный атрибут username, с помощью которого можно настроить персональное приветствие и напомнить пользователю о том, что вход был выполнен ❷. Затем выводится ссылка на страницу входа для пользователей, которые еще не прошли проверку ❸.
Учетная запись пользователя уже создана; попробуем ввести данные и посмотрим, работает ли страница. Откройте страницу http://localhost:8000/admin/. Если вы все еще работаете с правами администратора, то найдите ссылку logout в заголовке и щелкните на ней.
После выхода перейдите по адресу http://localhost:8000/accounts/login/. На экране должна появиться страница входа, похожая на рис. 19.4. Введите имя пользователя и пароль, заданные ранее, и вы снова должны оказаться на странице со списком. В заголовке страницы должно выводиться сообщение с указанием имени пользователя.
Рис. 19.4. Страница входа
Теперь необходимо предоставить пользователям возможность выйти из приложения. Запросы на выход из системы должны отправляться в виде POST-запросов, поэтому мы добавим небольшую форму выхода из системы в файл base.html. При щелчке на этой ссылке открывается страница, подтверждающая, что выход был выполнен успешно.
Форму для выхода из системы мы добавим в файл base.html, чтобы она была доступна на каждой странице, и добавим в другой блок if, чтобы ее было видно только пользователям, уже выполнившим вход:
base.html
--пропуск--
{% block content %}{% endblock content %}
{% if user.is_authenticated %}
❶ <hr />
❷ <form action="{% url 'accounts:logout' %}" method='post'>
{% csrf_token %}
<button name='submit'>Log out</button>
</form>
{% endif %}
По умолчанию для выхода из системы используется схема URL 'accounts/logout/'. Однако запрос должен быть отправлен по протоколу POST; в противном случае злоумышленники могут легко перехватить запрос на выход. Чтобы в запросах на выход из системы использовать протокол POST, мы определим простую форму.
Мы поместим форму в нижнюю часть страницы, под элементом горизонтальной прямой линии (<hr />) ❶. Это позволяет расположить кнопку выхода из системы гармонично под любым другим содержимым на странице. Сама форма имеет URL выхода из системы в аргументе action и значение 'post' в качестве метода запроса ❷. Все формы в Django должны содержать код {% csrf_token %}, даже такая простая форма, как наша. Она пуста, за исключением кнопки отправки запроса.
Когда пользователь нажимает кнопку выхода из системы, Django нужно знать, куда его отправить. Мы управляем этим поведением в файле settings.py:
settings.py
--пропуск--
# Мои настройки.
LOGIN_REDIRECT_URL = 'learning_logs:index'
LOGOUT_REDIRECT_URL = 'learning_logs:index'
Благодаря настройке LOGOUT_REDIRECT_URL, показанной выше, Django получает указание перенаправлять вышедших из системы пользователей обратно на главную страницу. Это простой способ подтвердить, что пользователь вышел из системы, поскольку после выхода он больше не должен видеть свое имя.
Теперь мы создадим страницу для регистрации новых пользователей. Для этой цели используем класс UserCreationForm, но напишем собственную функцию представления и шаблон.
Следующий код предоставляет шаблон URL для страницы регистрации, также размещенный в файле accounts/urls.py:
accounts/urls.py
"""Определяет схемы URL для пользователей."""
from django.urls import path, include
from . import views
app_name = 'accounts'
urlpatterns = [
# Включить URL авторизации по умолчанию.
path('', include('django.contrib.auth.urls')),
# Страница регистрации.
path('register/', views.register, name='register'),
]
Мы импортируем модуль views из accounts; он необходим, поскольку мы пишем собственное представление для страницы регрессии. Шаблон соответствует URL http://localhost:8000/accounts/register/ и отправляет запросы функции register(), которую мы сейчас напишем.
Данная функция должна вывести пустую форму регистрации при первом запросе страницы регистрации, а затем обрабатывает заполненную форму при отправке данных. Если регистрация прошла успешно, то функция должна выполнить вход для нового пользователя. Добавьте в файл accounts/views.py следующий код:
accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
def register(request):
"""Регистрирует нового пользователя."""
if request.method != 'POST':
# Вывод пустой формы регистрации.
❶ form = UserCreationForm()
else:
# Обработка заполненной формы.
❷ form = UserCreationForm(data=request.POST)
❸ if form.is_valid():
❹ new_user = form.save()
# Выполнение входа и перенаправление на главную страницу.
❺ login(request, new_user)
❻ return redirect('learning_logs:index')
# Вывод пустой или недействительной формы.
context = {'form': form}
return render(request, 'accounts/register.html', context)
Сначала импортируются функции render() и redirect(). Затем мы импортируем функцию login() для выполнения входа пользователя, если регистрационная информация верна. Вдобавок импортируется класс UserCreationForm по умолчанию. В функции register() мы проверяем, отвечает ли функция на запрос POST. Если нет, то создается экземпляр UserCreationForm, не содержащий исходных данных ❶.
В случае ответа на запрос POST создается экземпляр UserCreationForm, основанный на отправленных данных ❷. Мы проверяем, что они верны ❸; в данном случае — что имя пользователя содержит правильные символы, пароли совпадают, а пользователь не пытается вставить вредоносные конструкции в отправленные данные.
Если отправленные данные верны, то мы вызываем метод save() формы для сохранения имени пользователя и хеша пароля в базе данных ❹. Метод возвращает только что созданный объект пользователя, который сохраняется в new_user. После того как информация пользователя будет сохранена, мы выполняем вход; этот процесс состоит из двух шагов: сначала вызывается функция login() с объектами request и new_user ❺, которая создает действительный сеанс для нового пользователя. Наконец, пользователь перенаправляется на главную страницу ❻, где персонализированное приветствие в заголовке сообщает о том, что регистрация прошла успешно.
В конце функции создается страница, которая будет либо пустой формой, либо отправленной формой, содержащей недействительные данные.
Шаблон страницы регистрации похож на шаблон страницы входа. Проследите за тем, чтобы он был сохранен в одном каталоге с login.html:
register.html
{% extends "learning_logs/base.html" %}
{% block content %}
<form action="{% url 'accounts:register' %}" method='post'>
{% csrf_token %}
{{ form.as_div }}
<button name="submit">register</button>
</form>
{% endblock content %}
Шаблон должен выглядеть так же, как и другие шаблоны с формами, которые мы создали. Мы снова используем метод as_p, чтобы инфраструктура Django могла правильно отобразить все поля формы, в том числе все сообщения об ошибках, если форма была заполнена неправильно.
Следующий шаг — добавление кода, выводящего ссылку на страницу регистрации для любого пользователя, который еще не выполнил вход:
base.html
--пропуск--
{% if user.is_authenticated %}
Hello, {{ user.username }}.
{% else %}
<a href="{% url 'accounts:register' %}">Register</a> -
<a href="{% url 'accounts:login' %}">Log in</a>
{% endif %}
--пропуск--
Теперь пользователи, выполнившие вход, получат персональное приветствие и ссылку для выхода. Другие пользователи видят ссылку на страницу регистрации и ссылку для входа. Проверьте страницу регистрации, создав несколько учетных записей с разными именами пользователей.
В следующем разделе доступ к некоторым страницам будет ограничен, чтобы страницы были доступны только для зарегистрированных пользователей. Необходимо позаботиться и о том, чтобы каждая тема принадлежала конкретному пользователю.
ПРИМЕЧАНИЕ
Такая система регистрации позволяет любому пользователю создать сколько угодно учетных записей в «Журнале обучения». Однако некоторые системы требуют, чтобы пользователь подтвердил свою заявку, отправляя сообщение электронной почты, на которое он должен ответить. При таком подходе в системе будет создано меньше спамерских учетных записей, чем в простейшей системе из нашего примера. Но пока вы только учитесь создавать приложения, вполне нормально тренироваться на упрощенной системе регистрации вроде используемой нами.
Упражнения
19.2. Учетные записи в блоге. Добавьте систему аутентификации и регистрации в проект Blog, работа над которым началась в упражнении 19.1. Убедитесь, что пользователь, выполнивший вход, видит свое имя где-то на экране, а незарегистрированные пользователи — ссылку на страницу регистрации.
Пользователь должен иметь возможность вводить личные данные. Мы создадим систему, которая будет определять, какому пользователю принадлежат те или иные данные, и ограничивать доступ к страницам, чтобы пользователь мог работать только со своими данными.
В этом разделе мы изменим модель Topic, чтобы каждая тема принадлежала конкретному пользователю. При этом автоматически решается проблема с записями, поскольку каждая запись принадлежит конкретной теме. Начнем с ограничения доступа к страницам.
Django позволяет легко ограничить доступ к определенным страницам тем пользователям, которые выполнили вход. Для этого используется декоратор @login_required. Декоратор (decorator) представляет собой директиву, которая размещена непосредственно перед определением функции, применяется к ней перед ее выполнением и влияет на поведение кода. Рассмотрим пример.
Каждая тема будет принадлежать пользователю, поэтому только зарегистрированные пользователи смогут запрашивать страницы тем. Добавьте в learning_logs/views.py следующий код:
learning_logs/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
--пропуск--
@login_required
def topics(request):
"""Выводит все темы."""
--пропуск--
Сначала импортируется функция login_required(). Мы применяем login_required() как декоратор для функции представления topics(); для этого перед именем login_required() ставится знак @. Он сообщает Python, что этот код должен выполняться перед кодом topics().
Код login_required() проверяет, залогинился ли пользователь, и Django выполняет код topics() только при выполнении этого условия. В ином случае пользователь перенаправляется на страницу входа.
Чтобы перенаправление работало, необходимо внести изменения в файл settings.py и сообщить Django, где искать страницу входа. Добавьте в самый конец settings.py следующий фрагмент:
settings.py
--пропуск--
# Мои настройки.
LOGIN_REDIRECT_URL = 'learning_logs:index'
LOGOUT_REDIRECT_URL = 'learning_logs:index'
LOGIN_URL = 'accounts:login'
Когда пользователь, не прошедший проверку, запрашивает страницу, защищенную декоратором @login_required, Django отправляет пользователя на URL, определяемый LOGIN_URL в settings.py.
Чтобы протестировать эту возможность, завершите сеанс в любой из своих учетных записей и вернитесь на главную страницу. Щелкните на ссылке Topics (Темы), которая должна направить вас на страницу входа. Выполните вход с любой из своих учетных записей и на главной странице снова щелкните на ссылке Topics (Темы). На этот раз вы получите доступ к странице со списком тем.
Django упрощает ограничение доступа к страницам, но вы должны решить, какие страницы следует защищать. Лучше сначала подумать, к каким страницам можно разрешить полный доступ, а затем ограничить его для всех остальных страниц. Снять излишние ограничения несложно, причем это куда менее рискованно, чем оставлять действительно важные страницы в открытом доступе.
В приложении «Журнал обучения» мы оставим неограниченный доступ к главной странице, странице регистрации и выхода. Доступ ко всем остальным страницам будет ограничен.
Вот как выглядит файл learning_logs/views.py с декораторами @login_required, примененными к каждому представлению, кроме index():
learning_logs/views.py
--пропуск--
@login_required
def topics(request):
--пропуск--
@login_required
def topic(request, topic_id):
--пропуск--
@login_required
def new_topic(request):
--пропуск--
@login_required
def new_entry(request, topic_id):
--пропуск--
@login_required
def edit_entry(request, entry_id):
--пропуск--
Попробуйте обратиться к любой из этих страниц, не выполняя входа: вы будете перенаправлены обратно на страницу входа. Кроме того, вы не сможете щелкать на ссылках на такие страницы, как new_topic. Но если ввести URL http://localhost:8000/new_topic/, то вы будете перенаправлены на страницу входа. Ограничьте доступ ко всем URL, связанным с личными данными пользователей.
Теперь данные, отправленные пользователем, необходимо связать с тем пользователем, который их отправил. Связь достаточно установить только с данными, находящимися на высшем уровне иерархии, а низкоуровневые данные последуют за ними автоматически. Например, в приложении «Журнал обучения» на высшем уровне находятся темы, а каждая запись связывается с некой темой. Если каждая тема принадлежит конкретному пользователю, то мы сможем отследить владельца каждой записи в базе данных.
Изменим модель Topic и добавим для пользователя отношение по внешнему ключу. После этого необходимо провести миграцию базы данных. Наконец, необходимо изменить некоторые представления, чтобы в них отображались только данные, связанные с текущим пользователем.
В файле models.py изменяются всего две строки:
models.py
from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
"""Тема, которую изучает пользователь."""
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
"""Возвращает строковое представление модели."""
return self.text
class Entry(models.Model):
--пропуск--
Сначала модель User импортируется из django.contrib.auth. Затем в Topic добавляется поле owner, используемое в отношении по внешнему ключу для модели User. Если пользователь удаляется, то все связанные с ним темы также будут удалены.
При проведении миграции Django изменяет базу данных, чтобы в ней хранилась связь между каждой темой и пользователем. Для выполнения миграции Django необходимо знать, с каким пользователем должна быть связана каждая существующая тема. Проще всего связать все имеющиеся темы с одним пользователем, например суперпользователем. Но для этого сначала необходимо узнать его идентификатор.
Просмотрим идентификаторы всех пользователей, созданных до настоящего момента. Запустите сеанс оболочки Django и введите следующие команды:
(ll_env)learning_log$ python manage.py shell
❶ >>> from django.contrib.auth.models import User
❷ >>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>
❸ >>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>
В сеанс оболочки ❶ импортируется модель User. После этого просматриваются все пользователи, созданные до настоящего момента ❷. В выходных данных перечислены три пользователя: ll_admin, eric и willie.
Далее перебирается список пользователей, и для каждого выводятся его имя и идентификатор ❸. Когда Django спросит, с каким пользователем связать существующие темы, мы используем один из этих идентификаторов.
Зная значение идентификатора, можно провести миграцию базы данных. Когда вы это делаете, Python предлагает временно связать модель Topic с конкретным владельцем или добавить в файл models.py значение по умолчанию, которое сообщит, как следует поступить. Выберите вариант 1:
❶ (ll_env)learning_log$ python manage.py makemigrations learning_logs
❷ It is impossible to add a non-nullable field 'owner' to topic without
specifying a default. This is because...
❸ Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with
a null value for this column)
2) Quit and manually define a default value in models.py.
❹ Select an option: 1
❺ Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available...
Type 'exit' to exit this prompt
❻ >>> 1
Migrations for 'learning_logs':
learning_logs/migrations/0003_topic_owner.py
- Add field owner to topic
(ll_env)learning_log$
Сначала выдается команда makemigrations ❶. В ее выходных данных Django сообщает, что мы пытаемся добавить обязательное поле (значения которого отличаются от null) в существующую модель (topic) без указания значения по умолчанию ❷. Django предоставляет два варианта: мы можем либо указать значение по умолчанию прямо сейчас, либо завершить выполнение программы и добавить значение по умолчанию в models.py ❸. Мы выбираем первый вариант ❹. Тогда Django запрашивает значение по умолчанию ❺.
Чтобы связать все существующие темы с исходным административным пользователем ll_admin, я ввел идентификатор пользователя 1 ❻. Вы можете использовать идентификатор любого из созданных пользователей; он не обязан быть суперпользователем. Django проводит миграцию базы данных, используя это значение, и создает файл миграции 0003_topic_owner.py, добавляющий поле owner в модель Topic.
Теперь можно провести миграцию. Введите следующую команду в активной виртуальной среде:
(ll_env)learning_log$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
❶ Applying learning_logs.0003_topic_owner... OK
(ll_env)learning_log$
Django применяет новую миграцию с результатом OK ❶.
Чтобы убедиться в том, что миграция сработала ожидаемым образом, можно воспользоваться интерактивной оболочкой:
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
>>>
После импортирования Topic из learning_logs.models мы перебираем все существующие темы, выводим каждую из них и имя пользователя, которому она принадлежит. Как видите, сейчас каждая тема принадлежит пользователю ll_admin. (Если при выполнении кода произойдет ошибка, то попробуйте выйти из оболочки и запустить ее заново.)
ПРИМЕЧАНИЕ
Вместо выполнения миграции можно просто сбросить содержимое базы данных, но это приведет к потере всех существующих данных. Полезно научиться выполнять миграцию базы данных, не нарушая целостности данных пользователей. Если вы хотите начать с новой базы данных, то используйте команду python manage.py flush для повторного создания структуры базы данных. Вам придется создать нового суперпользователя, а все данные будут потеряны.
В настоящее время пользователь, выполнивший вход, будет видеть все темы независимо от того, под какой учетной записью он вошел. Сейчас мы изменим приложение, чтобы каждый пользователь видел только принадлежащие ему темы.
Внесите в функцию topics() в файле views.py следующее изменение:
learning_logs/views.py
--пропуск--
@login_required
def topics(request):
"""Выводит список тем."""
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
--пропуск--
Если пользователь выполнил вход, в объекте запроса устанавливается атрибут request.user с информацией о пользователе. Благодаря фрагменту кода Topic.objects.filter(owner=request.user) Django получает указание извлечь из базы данных только те объекты Topic, у которых атрибут owner соответствует текущему пользователю. Способ отображения остается прежним, поэтому изменять шаблон для страницы тем вообще не нужно.
Чтобы увидеть, как работает этот способ, выполните вход в качестве пользователя, с которым связаны все существующие темы, и перейдите к странице со списком тем. На ней должны отображаться все темы. Теперь завершите сеанс и войдите снова через другую учетную запись. На этот раз вы увидите сообщение No topics have been added yet (Темы пока не добавлены).
Никаких реальных ограничений на доступ к страницам еще не существует, поэтому любой зарегистрированный пользователь может опробовать разные URL (например, http://localhost:8000/topics/1/) и просмотреть страницы тем, которые ему удастся подобрать.
Попробуйте сделать это. После входа через учетную запись суперпользователя скопируйте URL или запишите идентификатор в URL темы, после чего завершите сеанс и войдите снова от имени другого пользователя. Введите URL этой темы. Вам удастся прочитать все записи, хотя сейчас вы вошли под именем другого пользователя.
Чтобы решить эту проблему, мы будем выполнять проверку перед получением запрошенных данных в функции представления topic():
learning_logs/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
❶ from django.http import Http404
--пропуск--
@login_required
def topic(request, topic_id):
"""Выводит одну тему и все ее записи."""
topic = Topic.objects.get(id=topic_id)
# Проверка того, что тема принадлежит текущему пользователю.
❷ if topic.owner != request.user:
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
--пропуск--
Код 404 — стандартное сообщение об ошибке, которое возвращается в тех случаях, когда запрошенный ресурс не существует на сервере. В данном случае мы импортируем исключение Http404 ❶, которое будет выдаваться программой при запросе пользователем темы, которую ему видеть не положено. Получив запрос темы, прежде чем отображать страницу, мы убеждаемся в том, что пользователь этой темы является текущим пользователем приложения. Если тема не принадлежит ему, то выдается исключение Http404 ❷, а Django возвращает страницу с ошибкой 404.
Пока при попытке просмотреть записи другого пользователя вы получите от Django сообщение Page Not Found (Страница не найдена). В главе 20 мы настроим проект так, чтобы пользователи видели полноценную страницу ошибки вместо страницы отладки.
Страницы edit_entry используют URL в форме http://localhost:8000/edit_entry/entry_id/, где entry_id — число. Защитим эту страницу, чтобы никто не мог подобрать URL для получения доступа к чужим записям:
learning_logs/views.py
--пропуск--
@login_required
def edit_entry(request, entry_id):
"""Редактирует существующую запись."""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user:
raise Http404
if request.method != 'POST':
--пропуск--
Программа читает запись и связанную с ней тему. Затем мы проверяем, совпадает ли владелец темы с текущим пользователем; при несовпадении выдается исключение Http404.
В настоящее время страница добавления новых тем несовершенна, поскольку не связывает новые темы с конкретным пользователем. При попытке добавить новую тему выдается сообщение об ошибке IntegrityError с уточнением NOT NULL constraint failed: learning_logs_topic.owner_id (Ограничение NOT NULL не выполнено: Learning_logs_topic.owner_id). Django сообщает, что при создании новой темы обязательно должно быть задано значение поля owner.
Проблема легко решается, поскольку мы можем получить доступ к информации текущего пользователя через объект request. Добавьте следующий код, связывающий новую тему с текущим пользователем:
learning_logs/views.py
--пропуск--
@login_required
def new_topic(request):
--пропуск--
else:
# Отправлены данные POST; обработать данные.
form = TopicForm(data=request.POST)
if form.is_valid():
❶ new_topic = form.save(commit=False)
❷ new_topic.owner = request.user
❸ new_topic.save()
return redirect('learning_logs:topics')
# Вывод пустой или недействительной формы.
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
--пропуск--
При первом вызове form.save() передается аргумент commit=False, поскольку новая тема должна быть изменена перед сохранением в базе данных ❶. Атрибуту owner новой темы присваивается текущий пользователь ❷. Наконец, мы вызываем save() для только что определенного экземпляра темы ❸. Теперь она содержит все обязательные данные, и ее сохранение пройдет успешно.
Вы сможете добавить сколько угодно новых тем для любого количества разных пользователей. Каждому из них будут доступны только его собственные данные, какие бы операции он ни пытался выполнять: просмотр данных, ввод новых или изменение существующих данных.
Упражнения
19.3. Рефакторинг. В файле views.py есть два места, в которых программа проверяет, что пользователь, связанный с темой, является текущим пользователем, вошедшим в систему. Поместите код этой проверки в функцию check_topic_owner() и вызовите ее при необходимости.
19.4. Защита страницы new_entry. Пользователь может попытаться добавить новую запись в журнал другого пользователя, вводя URL с идентификатором темы, принадлежащей другому пользователю. Чтобы предотвратить подобные атаки, перед сохранением новой записи проверьте, что текущий пользователь является владельцем темы, к которой относится запись.
19.5. Защищенный блог. В проекте Blog примите меры к тому, чтобы каждое сообщение было связано с конкретным пользователем. Убедитесь в том, что чтение всех сообщений доступно всем пользователям, но только зарегистрированные пользователи могут создавать новые сообщения и редактировать существующие. В представлении, в котором пользователи редактируют сообщения, перед обработкой формы убедитесь в том, что редактируемое сообщение принадлежит именно этому пользователю.
В этой главе вы научились использовать формы для создания новых тем и записей, а также редактирования существующих данных. Далее вы перешли к реализации системы учетных записей. Вы предоставили существующим пользователям возможность начинать и завершать сеанс работы с приложением, а также научились использовать класс Django UserCreationForm для создания новых учетных записей.
После создания простой системы аутентификации и регистрации пользователей вы ограничили доступ пользователей к некоторым страницам; для этого использовался декоратор @login_required. Затем связали данные с конкретными пользователями с помощью отношения по внешнему ключу. Вы также узнали, как выполнить миграцию базы данных, когда миграция требует ввести данные по умолчанию.
В последней части главы вы узнали, как ограничить состав данных, просматриваемых пользователем, с помощью функций представления. Для чтения соответствующих данных использовался метод filter(), а владелец запрашиваемых данных сравнивался с текущим пользователем.
Не всегда бывает сразу понятно, какие данные должны быть доступны всем пользователям, а какие данные следует защищать, но этот навык приходит с практикой. Решения, принятые нами в этой главе для защиты данных пользователей, наглядно показывают, почему при создании проекта желательно работать в команде: если кто-то просматривает код вашего проекта, это повышает вероятность выявления плохо защищенных областей.
К настоящему моменту вы создали полностью работоспособный проект, работающий на локальной машине. В последней главе вы усовершенствуете внешний вид приложения «Журнал обучения», чтобы оно выглядело более привлекательно. Кроме того, развернете проект на сервере, чтобы любой пользователь с доступом к Интернету мог зарегистрироваться и создать учетную запись.