Изучаем Python. Программирование игр, визуализация данных, веб-приложения

Мэтиз Эрик

19. Учетные записи пользователей

 

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

Затем будет реализована система проверки пользователей. Мы создадим страницу регистрации, на которой пользователи смогут создавать учетные записи, и ограничим доступ к некоторым страницам для анонимных пользователей. Затем некоторые функции представления будут изменены так, чтобы пользователь мог видеть только свои собственные данные. Вы узнаете, как обеспечить безопасность и конфиденциальность данных пользователей.

 

Редактирование данных

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

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

 

Добавление новых тем

Начнем с возможности создания новых тем. Страницы на базе форм добавляются практически так же, как и те страницы, которые мы уже строили ранее: вы определяете URL, пишете функцию представления и создаете шаблон. Принципиальное отличие — добавление нового модуля forms.py, содержащего функциональность форм.

 

Объект ModelForm

Любая страница, на которой пользователь может вводить и отправлять информацию, является формой, даже если на первый взгляд она на форму не похожа. Когда пользователь вводит информацию, необходимо проверить, что он ввел корректные данные, а не вредоносный код (например, код для нарушения работы сервера). Затем проверенная информация обрабатывается и сохраняется в нужном месте базы данных. Django автоматизирует бульшую часть этой работы.

Простейший способ построения форм в Django основан на использовании класса ModelForm, который автоматически строит форму на основании моделей, определенных в главе 18. Ваша первая форма будет создана в файле forms.py, который должен находиться в одном каталоге с models.py:

forms.py

from django import forms

from .models import Topic

(1) class TopicForm(forms.ModelForm):

. .class Meta:

(2) . . . .model = Topic

(3) . . . .fields = ['text']

(4) . . . .labels = {'text': ''}

Сначала импортируется модуль forms и модель, с которой мы будем работать: Topic. В точке (1) определяется класс с именем TopicForm, наследующий от forms.ModelForm. Простейшая версия ModelForm состоит из вложенного класса Meta, который сообщает Django, на какой модели должна базироваться форма и какие поля на ней должны находиться. В точке (2) форма создается на базе модели Topic, а на ней размещается только поле text (3). Код (4) приказывает Django не генерировать ­подпись для текстового поля.

 

URL-адрес для new_topic

URL-адрес новой страницы должен быть простым и содержательным, поэтому после того, как пользователь выбрал команду создания новой темы, он направляется по адресу http://localhost:8000/new_topic/. Ниже приведена схема URL для страницы new_topic, которая добавляется в learning_logs/urls.py:

urls.py

...

urlpatterns = [

...

. .# Страница для добавления новой темы

. .url(r'^new_topic/$', views.new_topic, name='new_topic'),

]

Эта схема URL будет отправлять запросы функции представления new_topic(), которую мы сейчас напишем.

 

Функция представления new_topic()

Функция new_topic() должна обрабатывать две разные ситуации: исходные запросы страницы new_topic (в этом случае должна отображаться пустая форма) и обработка данных, отправленных через форму. Затем она должна перенаправить пользователя обратно на страницу topics:

views.py

from django.shortcuts import render

from django.http import HttpResponseRedirect

from django.core.urlresolvers import reverse

from .models import Topic

from .forms import TopicForm

...

def new_topic(request):

. ."""Определяет новую тему."""

(1) . .if request.method != 'POST':

. . . .# Данные не отправлялись; создается пустая форма.

(2) . . . .form = TopicForm()

. .else:

. . . .# Отправлены данные POST; обработать данные.

(3) . . . .form = TopicForm(request.POST)

(4) . . . .if form.is_valid():

(5) . . . . . .form.save()

? . . . . . .return HttpResponseRedirect(reverse('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. Запросы GET используются для страниц, которые только читают данные с сервера, а запросы POST обычно используются в тех случаях, когда пользователь должен отправить информацию через форму. Для обработки всех наших форм будет использоваться метод POST (существуют и другие разновидности запросов, но в нашем проекте они не используются).

Функция new_topic() получает в параметре объект запроса. Когда пользователь впервые запрашивает эту страницу, его браузер отправляет запрос GET. Когда пользователь уже заполнил и отправил форму, его браузер отправляет запрос POST. В зависимости от типа запроса мы определяем, запросил ли пользователь пустую форму (запрос GET) или предлагает обработать заполненную форму (запрос POST).

Метод запроса — GET или POST — проверяется в точке (1) . Если метод запроса отличен от POST, вероятно, используется запрос GET, поэтому необходимо вернуть пустую форму (даже если это запрос другого типа, это все равно безопасно). Мы создаем экземпляр TopicForm (2), сохраняем его в переменной form и отправляем форму шаблону в словаре context ?. Так как при создании TopicForm аргументы не передавались, Django создает пустую форму, которая заполняется пользователем.

Если используется метод запроса POST, выполняется блок else, который ­обрабатывает данные, отправленные в форме. Мы создаем экземпляр TopicForm (3) и передаем ему данные, введенные пользователем, хранящиеся в request.POST. Возвращаемый объект form содержит информацию, отправленную пользователем.

Отправленную информацию нельзя сохранять в базе данных до тех пор, пока она не будет проверена (4). Функция is_valid() проверяет, что все обязательные поля были заполнены (все поля формы по умолчанию являются обязательными), а введенные данные соответствуют типам полей — например, что длина текста меньше 200 символов, как было указано в файле models.py в главе 18. Автоматическая проверка избавляет нас от большого объема работы. Если все данные действительны, можно вызвать метод save() (5), который записывает данные из формы в базу данных. После того как данные будут сохранены, страницу можно покинуть. Мы используем вызов reverse() для получения URL-адреса страницы topics и передаем его функции HttpResponseRedirect() ?, перенаправляющей браузер пользователя на страницу topics. На этой странице пользователь видит только что введенную им тему в общем списке тем.

 

Шаблон new_topic

Теперь создадим новый шаблон с именем new_topic.html для отображения только что созданной формы:

new_topic.html

{% extends "learning_logs/base.html" %}

{% block content %}

Add a new topic:

(1)

(2) . .{% csrf_token %}

(3) . .{{ form.as_p }}

(4) . .

. .

{% endblock content %}

Этот шаблон расширяет base.html, поэтому он имеет такую же базовую структуру, как и остальные страницы Learning Log. В точке (1) определяется форма HTML. Аргумент action сообщает серверу, куда передавать данные, отправленные формой; в данном случае данные возвращаются функции представления new_topic(). Аргумент method приказывает браузеру отправить данные в запросе типа POST.

Django использует шаблонный тег {% csrf_token %} (2) для предотвращения попыток получения несанкционированного доступа к серверу (атаки такого рода называются межсайтовой подделкой запросов). В точке (3) отображается форма; это наглядный пример того, насколько легко в Django выполняются такие стандартные операции, как отображение формы. Чтобы автоматически создать все поля, необходимые для отображения формы, достаточно включить шаблонную переменную {{ form.as_p }}. Модификатор as_p приказывает Django отобразить все элементы формы в формате абзацев — это простой способ аккуратного ­отображения формы.

Django не создает кнопку отправки данных для форм, поэтому мы определяем ее в точке (4).

 

Создание ссылки на страницу new_topic

Далее ссылка на страницу new_topic создается на странице topics:

topics.html

{% extends "learning_logs/base.html" %}

{% block content %}

Topics

    ...

Add a new topic:

{% endblock content %}

Разместите ссылку после списка существующих тем. Полученная форма изображена на рис. 19.1. Воспользуйтесь ею и добавьте несколько своих тем.

Рис. 19.1. Страница для добавления новой темы

 

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

Теперь, когда пользователь может добавлять новые темы, он также захочет добавлять новые записи. Мы снова определим URL, напишем новую функцию и шаблон и создадим ссылку на страницу. Но сначала нужно добавить в forms.py еще один класс.

 

Класс EntryForm

Мы должны создать форму, связанную с моделью 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']

(1) . . . .labels = {'text': ''}

(2) . . . .widgets = {'text': forms.Textarea(attrs={'cols': 80})}

Сначала в команду import к Topic добавляется Entry. Новый класс EntryForm наследует от forms.ModelForm и содержит вложенный класс Meta с указанием модели, на которой он базируется, и поле, включаемое в форму. Полю 'text' снова назначается пустая надпись (1) .

В точке (2) включается атрибут widgets. Виджет (widget) представляет собой элемент формы HTML: однострочное или многострочное текстовое поле, раскрывающийся список и т.д. Включая атрибут widgets, вы можете переопределить виджеты, выбранные Django по умолчанию. Приказывая Django использовать элемент forms.Textarea, мы настраиваем виджет ввода для поля 'text', чтобы ширина текстовой области составляла 80 столбцов вместо значения по умолчанию 40. У пользователя будет достаточно места для создания содержательных записей.

 

URL-адрес для new_entry

Необходимо включить аргумент topic_id в URL-адрес для создания новой записи, потому что запись должна ассоциироваться с конкретной темой. Вот как выглядит URL, который мы добавляем в learning_logs/urls.py:

urls.py

...

urlpatterns = [

...

. .# Страница для добавления новой записи

. .url(r'^new_entry/(?P\d+)/$', views.new_entry, name='new_entry'),

]

Эта схема URL соответствует любому URL-адресу в форме http://localhost:8000/new_entry/id/, где id — число, равное идентификатору темы. Выражение (?P\d+) захватывает числовое значение и сохраняет его в переменной topic_id. При запросе URL-адреса, соответствующего этой схеме, Django передает запрос и идентификатор темы функции представления new_entry().

 

Функция представления new_entry()

Функция представления new_entry очень похожа на функцию добавления новой темы:

views.py

from django.shortcuts import render

...

from .models import Topic

from .forms import TopicForm, EntryForm

...

def new_entry(request, topic_id):

. ."""Добавляет новую запись по конкретной теме."""

(1) . .topic = Topic.objects.get(id=topic_id)

. .

(2) . .if request.method != 'POST':

. . . .# Данные не отправлялись; создается пустая форма.

(3) . . . .form = EntryForm() . . . .

. .else:

. . . .# Отправлены данные POST; обработать данные.

(4) . . . .form = EntryForm(data=request.POST)

. . . .if form.is_valid():

(5) . . . . . .new_entry = form.save(commit=False)

? . . . . . .new_entry.topic = topic

. . . . . .new_entry.save()

? . . . . . .return HttpResponseRedirect(reverse('learning_logs:topic',

. . . . . . . . . . . . . . . . . . . .args=[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 для получения правильного объекта темы (1) .

В точке (2) проверяется метод запроса: POST или GET. Блок if выполняется для запроса GET, и мы создаем пустой экземпляр EntryForm (3). Для метода запроса POST мы обрабатываем данные, создавая экземпляр EntryForm, заполненный данными POST из объекта запроса (4). Затем проверяется корректность данных формы. Если данные корректны, необходимо задать атрибут topic объекта записи перед сохранением его в базе данных.

При вызове save() мы включаем аргумент commit=False (5) для того, чтобы создать новый объект записи и сохранить его в new_entry, не сохраняя пока в базе данных. Мы присваиваем атрибуту topic объекта new_entry тему, прочитанную из базы данных в начале функции ?, после чего вызываем save() без аргументов. В результате запись сохраняется в базе данных с правильной ассоциированной темой.

В точке ? пользователь перенаправляется на страницу темы. При вызове reverse() должны передаваться два аргумента: имя схемы URL, для которой генерируется URL-адрес, и список аргументов со всеми аргументами, которые должны быть включены в URL. Список аргументов содержит всего один элемент topic_id. Вызов HttpResponseRedirect() перенаправляет пользователя на страницу темы, для которой была создана запись, и пользователь видит новую запись в списке записей.

 

Шаблон new_entry

Как видно из следующего кода, шаблон new_entry похож на шаблон new_topic:

new_entry.html

{% extends "learning_logs/base.html" %}

{% block content %}

(1)

{{ topic }}

. .

Add a new entry:

(2)

. .{% csrf_token %}

. .{{ form.as_p }}

. .

. .

{% endblock content %}

В начале страницы выводится тема (1) , чтобы пользователь мог видеть, в какую тему добавляется новая запись. Тема также служат ссылкой для возврата к основной странице этой темы.

Аргумент action формы включает значение topic_id из URL, чтобы функция ­представления могла связать новую запись с правильной темой (2). В остальном этот шаблон почти не отличается от new_topic.html.

 

Создание ссылки на страницу new_entry

Затем необходимо создать ссылку на страницу new_entry на каждой странице темы:

topic.html

{% extends "learning_logs/base.html" %}

{% block content %}

Topic: {{ topic }}

Entries:

. .add new entry

    ...

{% endblock content %}

Ссылка добавляется перед выводом записей, потому что добавление новой записи является самым частым действием на этой странице. На рис. 19.2 изображена страница new_entry. Теперь пользователь может добавить сколько угодно новых тем и новых записей по каждой теме. Опробуйте страницу new_entry, добавив несколько записей для каждой из созданных вами тем.

Рис. 19.2. Страница new_entry

 

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

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

 

URL-адрес для edit_entry

В URL-адресе страницы должен передаваться идентификатор редактируемой ­записи. В файл learning_logs/urls.py для этого вносятся следующие изменения:

urls.py

...

urlpatterns = [

...

. .# Страница для редактирования записи

. .url(r'^edit_entry/(?P\d+)/$', views.edit_entry,

. . . .name='edit_entry'),

]

Идентификатор, переданный в URL (например, http://localhost:8000/edit_entry/1/), сохраняется в параметре entry_id. Схема The URL отправляет запросы, соответствующие этому формату, функции представления edit_entry().

 

Функция представления edit_entry()

Когда страница edit_entry получает запрос GET, edit_entry() возвращает форму для редактирования записи. При получении запроса POST с отредактированной записью страница сохраняет измененный текст в базе данных:

views.py

from django.shortcuts import render

...

from .models import Topic, Entry

from .forms import TopicForm, EntryForm

...

def edit_entry(request, entry_id):

. ."""Редактирует существующую запись."""

(1) . .entry = Entry.objects.get(id=entry_id)

. .topic = entry.topic

. .

. .if request.method != 'POST':

. . . .# Исходный запрос; форма заполняется данными текущей записи.

(2) . . . .form = EntryForm(instance=entry)

. .else:

. . . .# Отправка данных POST; обработать данные.

(3) . . . .form = EntryForm(instance=entry, data=request.POST)

. . . .if form.is_valid():

(4) . . . . . .form.save()

(5) . . . . . .return HttpResponseRedirect(reverse('learning_logs:topic',

. . . . . . . . . . . . . . . . . . . .args=[topic.id]))

. .

. .context = {'entry': entry, 'topic': topic, 'form': form}

. .return render(request, 'learning_logs/edit_entry.html', context)

Сначала необходимо импортировать модель Entry. В точке (1) мы получаем объект записи, который пользователь хочет изменить, и тему, связанную с этой записью. В блоке if, который выполняется для запроса GET, создается экземпляр EntryForm с аргументом instance=entry (2). Этот аргумент приказывает Django создать форму, заранее заполненную информацией из существующего объекта записи. Пользователь видит свои существующие данные и может отредактировать их.

При обработке запроса POST передаются аргументы instance=entry и data=request.POST (3), чтобы приказать Django создать экземпляр формы на основании информации существующего объекта записи, обновленный данными из request.POST. Затем проверяется корректность данных формы. Если данные корректны, следует вызов save() без аргументов (4). Далее происходит перенаправление на страницу темы (5), и пользователь видит обновленную версию отредактированной им записи.

 

Шаблон edit_entry

Шаблон edit_entry.html очень похож на new_entry.html:

edit_entry.html

{% extends "learning_logs/base.html" %}

{% block content %}

{{ topic }}

. .

Edit entry:

. .

(1)

. .{% csrf_token %}

. .{{ form.as_p }}

(2) . .

{% endblock content %}

В точке (1) аргумент action отправляет форму функции edit_entry() для обработки. Идентификатор записи включается как аргумент в тег {% url %} , чтобы функция представления могла изменить правильный объект записи. Кнопка отправки данных создается с текстом, который напоминает пользователю, что он сохраняет изменения, а не создает новую запись (2).

 

Создание ссылки на страницу edit_entry

Теперь необходимо включить ссылку на страницу edit_entry в каждую тему на странице со списком тем:

topic.html

...

{% for entry in entries %}

  • {{ entry.date_added|date:'M d, Y H:i' }}

    {{ entry.text|linebreaks }}

    . .

    . . . .edit entry

    . .

  • ...

    После даты и текста каждой записи включается ссылка редактирования. Мы используем шаблонный тег {% url %} для определения схемы URL из именованной схемы edit_entry и идентификатора текущей записи в цикле (entry.id). Текст ссылки "edit entry" выводится после каждой записи на странице. На рис. 19.3 показано, как выглядит страница со списком тем с этими ссылками.

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

    Рис. 19.3. Каждая запись снабжается ссылкой для редактирования этой записи

    ­регистрации пользователей, чтобы любой желающий мог создать свою учетную запись в Learning Log и ввести собственный набор тем и записей.

    Упражнения

    19-1. Блог: создайте новый проект Django с именем Blog. Создайте в проекте приложение с именем blogs и моделью BlogPost. Модель должна содержать такие поля, как title, text и date_added. Создайте суперпользователя для проекта и воспользуйтесь административным сайтом для создания пары коротких сообщений. Создайте домашнюю страницу, на ­которой выводятся все сообщения в хронологическом порядке.

    Создайте одну форму для создания новых сообщений и другую форму для редактирования существующих сообщений. Заполните формы и убедитесь в том, что они работают.

     

    Создание учетных записей пользователей

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

     

    Приложение users

    Начнем с создания нового приложения users командой startapp:

    (ll_env)learning_log$ python manage.py startapp users

    (ll_env)learning_log$ ls

    (1) db.sqlite3 learning_log learning_logs ll_env manage.py users

    (ll_env)learning_log$ ls users

    (2)admin.py __init__.py migrations models.py tests.py views.py

    Эта команда создает новый каталог с именем users (1) , структура которого повторяет структуру каталогов приложения learning_logs (2).

     

    Добавление пользователей в settings.py

    Новое приложение необходимо добавить в settings.py:

    settings.py

    ...

    INSTALLED_APPS = (

    ...

    # My apps

    'learning_logs',

    . .'users',

    )

    ...

    Django включает приложение users в общий проект.

     

    Включение URL-адресов из users

    Затем необходимо изменить корневой файл urls.py, чтобы он включал URL-адреса, написанные для приложения users:

    urls.py

    from django.conf.urls import include, url

    from django.contrib import admin

    urlpatterns = [

    url(r'^admin/', include(admin.site.urls)),

    . .url(r'^users/', include('users.urls', namespace='users')),

    url(r'', include('learning_logs.urls', namespace='learning_logs')),

    ]

    Добавим строку для включения файла urls.py из users. Эта строка будет соответствовать любому URL-адресу, начинающемуся со слова users, — например, http://localhost:8000/users/login/. Также будет создано пространство имен 'users', чтобы мы могли отличать URL-адреса, принадлежащие приложению learning_logs, от URL, принадлежащих приложению users.

     

    Страница входа

    Начнем с реализации страницы входа. Мы воспользуемся стандартным представлением login, которое предоставляет Django, так что шаблон URL выглядит немного иначе. Создайте новый файл urls.py в каталоге learning_log/users/ и добавьте в него следующий код:

    urls.py

    """Определяет схемы URL для пользователей"""

    from django.conf.urls import url

    (1) from django.contrib.auth.views import login

    from . import views

    urlpatterns = [

    . .# Страница входа

    (2) . .url(r'^login/$', login, {'template_name': 'users/login.html'},

    . . . .name='login'),

    ]

    Сначала импортируется представление login по умолчанию (1) . Схема страницы входа соответствует URL http://localhost:8000/users/login/ (2). Когда Django читает этот URL-адрес, слово users указывает, что следует обратиться к users/urls.py, а login сообщает о том, что запросы должны отправляться представлению login по умолчанию (обратите внимание: в аргументе представления используется login, а не views.login). Так как мы не пишем собственную функцию представления, мы передаем словарь, который сообщает Django, где искать шаблон (сейчас мы его напишем). Этот шаблон будет частью приложения users, а не приложения learning_logs.

     

    Шаблон login

    Когда пользователь запрашивает страницу входа, Django использует свое представление login по умолчанию, но мы все равно должны предоставить шаблон для этой страницы. В каталоге learning_log/users/ создайте каталог с именем templates, а внутри него — еще один каталог с именем users. Вот как выглядит шаблон login.html, который должен находиться в learning_log/users/templates/users/:

    login.html

    {% extends "learning_logs/base.html" %}

    {% block content %}

    (1) {% if form.errors %}

    Your username and password didn't match. Please try again.

    {% endif %}

    . .

    (2)

    {% csrf_token %}

    (3) {{ form.as_p }}

    . .

    (4)

    (5)

    . .

    {% endblock content %}

    Шаблон расширяет base.html, чтобы страница входа по оформлению и поведению была похожа на другие страницы сайта. Обратите внимание: шаблон в одном приложении может расширять шаблон из другого приложения.

    Если у формы установлен атрибут errors, выводится сообщение об ошибке (1) . В нем говорится, что комбинация имени пользователя и пароля не соответствует информации, хранящейся в базе данных.

    Мы хотим, чтобы представление обработало форму, поэтому аргументу action присваивается URL страницы входа (2). Представление отправляет форму шаблону, мы должны вывести форму (3) и добавить кнопку отправки данных (4). В точке (5) включается скрытый элемент формы 'next'; аргумент value сообщает Django, куда перенаправить пользователя после успешно выполненного входа. В нашем случае пользователь возвращается обратно на домашнюю страницу.

     

    Создание ссылки на страницу входа

    Добавим ссылку на страницу входа в base.html, чтобы она присутствовала на каждой странице. Ссылка не должна отображаться, если пользователь уже прошел процедуру входа, поэтому она вкладывается в тег {% if %}:

    base.html

    Learning Log -

    Topics -

    (1) {% if user.is_authenticated %}

    (2) . .Hello, {{ user.username }}.

    {% else %}

    (3) . .log in

    {% endif %}

    {% block content %}{% endblock content %}

    В системе аутентификации Django в каждом шаблоне доступна переменная user, в которой всегда присутствует атрибут is_authenticated: атрибут равен True, если пользователь прошел проверку, и False в противном случае. Это позволяет вам выводить разные сообщения для проверенных и непроверенных пользователей.

    В данном случае мы выводим приветствие для пользователей, выполнивших вход. У проверенных пользователей устанавливается дополнительный атрибут username, который обеспечит личную настройку приветствия и напомнит пользователю о том, что вход был выполнен. В точке (3) выводится ссылка на страницу входа для пользователей, которые еще не прошли проверку.

     

    Использование страницы входа

    Учетная запись пользователя уже создана; попробуем ввести данные и посмотрим, работает ли страница. Откройте страницу http://localhost:8000/admin/. Если вы все еще работаете с правами администратора, найдите ссылку выхода в заголовке и щелкните на ней.

    После выхода перейдите по адресу http://localhost:8000/users/login/. На экране должна появиться страница входа, похожая на рис. 19.4. Введите имя пользователя и пароль, заданные ранее, и вы снова должны оказаться на странице со списком.

    Рис. 19.4. Страница входа

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

     

    Выход

    Теперь необходимо предоставить пользователям возможность выхода из приложения. Мы не будем строить отдельную страницу для выхода; пользователь просто щелкает на ссылке и возвращается к домашней странице. Мы определяем схему URL для ссылки выхода, пишем функцию представления и предоставляем ссылку выхода в base.html.

     

    URL-адрес выхода

    Следующий код определяет схему URL для выхода, соответствующую URL http://localhost:8000/users/logout/. Файл users/urls.py выглядит так:

    urls.py

    ...

    urlpatterns = [

    # Страница входа

    ...

    . .# Страница выхода

    . .url(r'^logout/$', views.logout_view, name='logout'),

    ]

    Схема URL отправляет запрос функции logout_view(), имя которой выбрано так, чтобы оно отличалось от имени функции logout(), вызываемой из представления. (Проследите за тем, чтобы изменения вносились в файл users/urls.py, а не в файл learning_log/urls.py.)

     

    Функция представления logout_view()

    Функция logout_view() тривиальна: мы просто импортируем функцию Django logout(), вызываем ее, а затем возвращаем пользователя на домашнюю страницу. Откройте файл users/views.py и введите следующий код:

    views.py

    from django.http import HttpResponseRedirect

    from django.core.urlresolvers import reverse

    (1) from django.contrib.auth import logout

    def logout_view(request):

    . ."""Завершает сеанс работы с приложением."""

    (2) . .logout(request)

    (3) . .return HttpResponseRedirect(reverse('learning_logs:index'))

    Мы импортируем функцию logout() из django.contrib.auth (1) . В функции вызывается функция logout() (2), в аргументе которой должен передаваться объект запроса. Затем происходит возврат к домашней странице (3).

     

    Ссылка на представление выхода

    Теперь нужно создать ссылку для выхода. Мы включим ее в base.html, чтобы она была доступна на каждой странице, и включим в секцию {% if user.is_authenticated %}, чтобы ссылка была видна только пользователям, уже выполнившим вход:

    base.html

    ...

    {% if user.is_authenticated %}

    Hello, {{ user.username }}.

    . .log out

    {% else %}

    log in

    {% endif %}

    ...

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

    Рис. 19.5. Домашняя страница с персональным приветствием и ссылкой для выхода

     

    Страница регистрации

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

     

    URL-адрес регистрации

    Следующий код предоставляет шаблон URL для страницы регистрации — также в файле users/urls.py:

    urls.py

    ...

    urlpatterns = [

    # Страница входа

    ...

    . .# Страница регистрации

    . .url(r'^register/$', views.register, name='register'),

    ]

    Шаблон соответствует URL http://localhost:8000/users/register/ и отправляет запросы функции register(), которую мы сейчас напишем.

     

    Функция представления register()

    Функция представления register() должна вывести пустую форму регистрации при первом запросе страницы регистрации, а затем обрабатывает заполненную форму регистрации при отправке данных. Если регистрация прошла успешно, функция также должна выполнить вход для нового пользователя. Включите следующий код в users/views.py:

    views.py

    from django.shortcuts import render

    from django.http import HttpResponseRedirect

    from django.core.urlresolvers import reverse

    from django.contrib.auth import login, logout, authenticate

    from django.contrib.auth.forms import UserCreationForm

    def logout_view(request):

    ...

    def register(request):

    . ."""Регистрирует нового пользователя."""

    . .if request.method != 'POST':

    . . . .# Display blank registration form.

    (1) . . . .form = UserCreationForm()

    . .else:

    . . . .# Обработка заполненной формы.

    (2) . . . .form = UserCreationForm(data=request.POST)

    . . . .

    (3) . . . .if form.is_valid():

    (4) . . . . . .new_user = form.save()

    . . . . . .# Выполнение входа и перенаправление на домашнюю страницу.

    (5) . . . . . .authenticated_user = authenticate(username=new_user.username,

    . . . . . . . .password=request.POST['password1'])

    ? . . . . . .login(request, authenticated_user)

    ? . . . . . .return HttpResponseRedirect(reverse('learning_logs:index'))

    . .context = {'form': form}

    . .return render(request, 'users/register.html', context)

    Сначала импортируется функция render(), после чего импортируются функции login() и authenticate() для выполнения входа пользователя, если регистрационная информация верна. Также импортируется класс UserCreationForm по умол­чанию. В функции register() мы проверяем, отвечает ли функция на запрос POST. Если нет, создается экземпляр UserCreationForm, не содержащий исходных данных (1) . В случае ответа на запрос POST создается экземпляр UserCreationForm, основанный на отправленных данных (2). Мы проверяем, что данные верны (3); в данном случае что имя пользователя содержит правильные символы, пароли совпадают, а пользователь не пытается вставить вредоносные конструкции в отправленные данные.

    Если отправленные данные верны, мы вызываем метод save() формы для сохранения имени пользователя и хеша пароля в базе данных (4). Метод save() возвращает только что созданный объект пользователя, который сохраняется в new_user.

    После того как информация пользователя будет сохранена, мы выполняем вход; этот процесс состоит из двух шагов: сначала вызывается функция authenticate() с аргументом new_user.username и паролем (5). При регистрации пользователю предлагается ввести два совпадающих пароля; поскольку данные формы верны, мы знаем, что пароли совпадают, и можем использовать любой из них. В данном случае используется значение, связанное с ключом 'password1' в данных POST формы. Если имя пользователя и пароль верны, метод возвращает проверенный объект пользователя, который сохраняется в authenticated_user. Затем вызывается функция login() с объектами request и authenticated_user ?, которая создает действительный сеанс для нового пользователя.

    Наконец, пользователь перенаправляется на домашнюю страницу ?, где приветствие в заголовке сообщает о том, что регистрация прошла успешно.

     

    Шаблон регистрации

    Шаблон страницы регистрации похож на шаблон страницы входа. Проследите за тем, чтобы он был сохранен в одном каталоге с login.html:

    register.html

    {% extends "learning_logs/base.html" %}

    {% block content %}

    . .{% csrf_token %}

    . .{{ form.as_p }}

    . . . .

    . .

    . .

    . .

    {% endblock content %}

    Мы снова используем метод as_p, чтобы инфраструктура Django могла правильно отобразить все поля формы, включая все сообщения об ошибках, если форма была заполнена неправильно.

     

    Создание ссылки на страницу регистрации

    Следующий шаг — добавление кода для вывода ссылки на страницу регистрации для любого пользователя, еще не выполнившего вход:

    base.html

    ...

    {% if user.is_authenticated %}

    Hello, {{ user.username }}.

    log out

    {% else %}

    . .register -

    log in

    {% endif %}

    ...

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

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

    Примечание

    Такая система регистрации позволяет любому пользователю создать сколько угодно учетных записей Learning Log. Однако некоторые системы требуют, чтобы пользователь подтвердил свою заявку, отправляя сообщение электронной почты, на которое пользователь должен ответить. При ­таком подходе в системе будет создано меньше спамерских учетных записей, чем в простейшей системе из нашего примера. Но пока вы только учитесь строить приложения, вполне нормально тренироваться на упрощенной системе регистрации вроде используемой нами.

    Упражнения

    19-2. Учетные записи в блоге: добавьте систему аутентификации и регистрации в проект Blog, работа над которым началась в упражнении 19-1 (с. 419). Проследите за тем, чтобы пользователь, выполнивший вход, видел свое имя где-то на экране, а незарегистрированные пользователи видели ссылку на страницу регистрации.

     

    Редактирование данных

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

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

     

    Ограничение доступа с использованием @login_required

    Django позволяет легко ограничить доступ к определенным страницам для пользователей, выполнивших вход, с помощью декоратора @login_required. Декоратор (decorator) представляет собой директиву, размещенную непосредственно перед определением функции, применяемую к функции перед ее выполнением и влияющую на поведение кода. Рассмотрим пример.

     

    Ограничение доступа к страницам тем

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

    views.py

    ...

    from django.core.urlresolvers import reverse

    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

    """

    Django settings for learning_log project

    ...

    # Мои настройки

    LOGIN_URL = '/users/login/'

    Когда пользователь, не прошедший проверку, запрашивает страницу, защищенную декоратором @login_required, Django отправляет пользователя на URL-адрес, определяемый LOGIN_URL в settings.py.

    Чтобы протестировать эту возможность, завершите сеанс в любой из своих учетных записей и вернитесь на домашнюю страницу. Щелкните на ссылке Topics, которая должна направить вас на страницу входа. Выполните вход с любой из своих учетных записей, на домашней странице снова щелкните на ссылке Topics. На этот раз вы получите доступ к странице со списком тем.

     

    Ограничение доступа в Learning Log

    Django упрощает ограничение доступа к страницам, но вы должны решить, какие страницы следует защищать. Лучше сначала подумать, к каким страницам можно разрешить неограниченный доступ, а затем ограничить его для всех остальных страниц. Снять излишние ограничения несложно, причем это куда менее рискованно, чем оставлять действительно важные страницы без ограничения доступа.

    В приложении Learning Log мы оставим неограниченный доступ к домашней странице, странице регистрации и выхода. Доступ ко всем остальным страницам будет ограничен.

    Вот как выглядит файл learning_logs/views.py с декораторами @login_required, примененными к каждому представлению, кроме index():

    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-адресам, связанным с личными данными пользователей.

     

    Связывание данных с конкретными пользователями

    Теперь данные, отправленные пользователем, необходимо связать с тем пользователем, который их отправил. Связь достаточно установить только с данными, находящимися на высшем уровне иерархии, а низкоуровневые данные последуют за ними автоматически. Например, в приложении Learning Log на высшем уровне находятся темы, а каждая запись связывается с некоторой темой. Если каждая тема принадлежит конкретному пользователю, мы сможем отследить владельца каждой записи в базе данных.

    Изменим модель Topic и добавим отношение внешнего ключа с пользователем. После этого необходимо провести миграцию базы данных. Наконец, необходимо изменить некоторые представления, чтобы в них отображались только данные, связанные с текущим пользователем.

     

    Изменение модели 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)

    def __str__(self):

    """Возвращает строковое представление модели."""

    return self.text

    class Entry(models.Model):

    ...

    Сначала модель User импортируется из django.contrib.auth. Затем в Topic добавляется поле owner, используемое в отношении внешнего ключа с моделью User.

     

    Идентификация существующих пользователей

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

    Просмотрим идентификаторы всех пользователей, созданных до настоящего момента. Запустите сеанс оболочки Django и введите следующие команды:

    (venv)learning_log$ python manage.py shell

    (1) >>> from django.contrib.auth.models import User

    (2)>>> User.objects.all()

    [, , ]

    (3)>>> for user in User.objects.all():

    ... . . print(user.username, user.id)

    ...

    ll_admin 1

    eric 2

    willie 3

    >>>

    В точке (1) в сеанс оболочки импортируется модель User. После этого просматриваются все пользователи, созданные до настоящего момента (2). В выходных данных перечислены три пользователя: ll_admin, eric и willie.

    В точке (3) перебирается список пользователей, и для каждого пользователя выводится его имя и идентификатор. Когда Django спросит, с каким пользователем связать существующие темы, мы используем один из этих идентификаторов.

     

    Миграция базы данных

    Зная значение идентификатора, можно провести миграцию базы данных.

    (1) (venv)learning_log$ python manage.py makemigrations learning_logs

    (2)You are trying to add a non-nullable field 'owner' to topic without a default;

    we can't do that (the database needs something to populate existing rows).

    (3)Please select a fix:

    1) Provide a one-off default now (will be set on all existing rows)

    2) Quit, and let me add a default in models.py

    (4)Select an option: 1

    (5)Please enter the default value now, as valid Python

    The datetime and django.utils.timezone modules are available, so you can do

    e.g. timezone.now()

    ? >>> 1

    Migrations for 'learning_logs':

    0003_topic_owner.py:

    . .- Add field owner to topic

    Сначала выдается команда makemigrations (1) . В ее выходных данных (2) Django сообщает, что мы пытаемся добавить обязательное поле (значения которого отличны от null) в существующую модель (topic) без указания значения по умолчанию. Django предоставляет два варианта (3): мы можем либо указать значение по умолчанию прямо сейчас, либо завершить выполнение программы и добавить значение по умолчанию в models.py. В точке (4) выбирается первый вариант. Тогда Django запрашивает значение по умолчанию (5).

    Чтобы связать все существующие темы с исходным административным пользователем ll_admin, я ввел в точке ? идентификатор пользователя 1. Вы можете использовать идентификатор любого из созданных пользователей; он не обязан быть суперпользователем. Django проводит миграцию базы данных, используя это значение, и создает файл миграции 0003_topic_owner.py, добавляющий поле owner в модель Topic.

    Теперь можно провести миграцию. Введите следующую команду в активной виртуальной среде:

    (venv)learning_log$ python manage.py migrate

    Operations to perform:

    Synchronize unmigrated apps: messages, staticfiles

    Apply all migrations: learning_logs, contenttypes, sessions, admin, auth

    ...

    Running migrations:

    Rendering model states... DONE

    (1) Applying learning_logs.0003_topic_owner... OK

    (venv)learning_log$

    Django применяет новую миграцию с результатом OK (1) . Чтобы убедиться в том, что миграция сработала так, как и ожидалось, можно воспользоваться интерактивной оболочкой:

    (1) >>> from learning_logs.models import Topic

    (2)>>> for topic in Topic.objects.all():

    ... . . print(topic, topic.owner)

    ...

    Chess ll_admin

    Rock Climbing ll_admin

    >>>

    После импортирования Topic из learning_logs.models (1) мы перебираем все существующие темы, выводим каждую тему и имя пользователя, которому она принадлежит (2). Как видите, сейчас каждая тема принадлежит пользователю ll_admin.

    Примечание

    Вместо миграции можно просто сбросить содержимое базы данных, но это приведет к потере всех существующих данных. Полезно научиться выполнять миграцию базы данных без нарушения целостности данных пользователей. Если вы хотите начать с новой базы данных, используйте команду python manage.py flush для повторного построения структуры базы данных. Вам придется создать нового суперпользователя, а все данные будут потеряны.

     

    Ограничение доступа к темам

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

    Внесите следующее изменение в функцию topics() в файле views.py:

    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 соответствует текущему пользователю. Так как способ отображения не изменяется, изменять шаблон для страницы тем вообще не нужно.

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

     

    Защита тем пользователя

    Никаких реальных ограничений на доступ к страницам еще не существует, поэтому любой зарегистрированный пользователь может опробовать разные URL (например, http://localhost:8000/topics/1/) и просмотреть страницы тем, которые ему удастся подобрать.

    Попробуйте сделать это. После входа с учетной записью суперпользователя скопируйте URL или запишите идентификатор в URL темы, после чего завершите сеанс и войдите снова от имени другого пользователя. Введите URL этой темы. Вам удастся прочитать все записи, хотя сейчас вы вошли под именем другого пользователя.

    Чтобы решить эту проблему, мы будем выполнять проверку перед получением запрошенных данных в функции представления topic():

    views.py

    from django.shortcuts import render

    (1) from django.http import HttpResponseRedirect, Http404

    from django.core.urlresolvers import reverse

    ...

    @login_required

    def topic(request, topic_id):

    """Выводит одну тему и все ее записи."""

    topic = Topic.objects.get(id=topic_id)

    . .# Проверка того, что тема принадлежит текущему пользователю.

    (2) . .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 (1) , которое будет выдаваться программой при запросе пользователем темы, которую ему видеть не положено. Получив запрос темы, перед отображением страницы мы убеждаемся в том, что пользователь этой темы является текущим пользователем приложения. Если тема не принадлежит текущему пользователю, выдается исключение Http404 (2), а Django возвращает страницу с ошибкой 404.

    Пока при попытке просмотреть записи другого пользователя вы получите от Django сообщение «Страница не найдена». В главе 20 проект будет настроен так, чтобы пользователь видел полноценную страницу ошибки.

     

    Защита страницы edit_entry

    Страницы edit_entry используют URL-адреса в форме http://localhost:8000/edit_entry/entry_id/, где entry_id — число. Защитим эту страницу, чтобы никто не мог подобрать URL для получения доступа к чужим записям:

    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 с уточнением learning_logs_topic.user_id may not be NULL. Django говорит, что при создании новой темы обязательно должно быть задано значение поля owner.

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

    views.py

    ...

    @login_required

    def new_topic(request):

    """Определяет новую тему."""

    if request.method != 'POST':

    # Данные не отправлялись; создается пустая форма.

    form = TopicForm()

    else:

    # Отправлены данные POST; обработать данные.

    form = TopicForm(request.POST)

    if form.is_valid():

    (1) . . . . . .new_topic = form.save(commit=False)

    (2) . . . . . .new_topic.owner = request.user

    (3) . . . . . .new_topic.save()

    return HttpResponseRedirect(reverse('learning_logs:topics'))

    . . . . . .

    context = {'form': form}

    return render(request, 'learning_logs/new_topic.html', context)

    ...

    При первом вызове form.save() передается аргумент commit=False, потому что новая тема должна быть изменена перед сохранением в базе данных (1) . Атрибуту owner новой темы присваивается текущий пользователь (2). Наконец, мы вызываем save() для только что определенного экземпляра темы (3). Теперь тема содержит все обязательные данные, и ее сохранение пройдет успешно.

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

    Упражнения

    19-3. Рефакторинг: в views.py есть два места, в которых программа проверяет, что пользователь, связанный с темой, является текущим пользователем. Поместите код этой проверки в функцию с именем check_topic_owner() и вызовите эту функцию при необходимости.

    19-4. Защита new_entry: пользователь может попытаться добавить новую запись в журнал другого пользователя, вводя URL-адрес с идентификатором темы, принадлежащей другому пользователю. Чтобы предотвратить подобные атаки, перед сохранением новой записи проверьте, что текущий пользователь является владельцем темы, к которой относится запись.

    19-5. Защищенный блог: в проекте Blog примите меры к тому, чтобы каждое сообщение в блоге было связано с конкретным пользователем. Убедитесь в том, что чтение всех сообщений доступно всем пользователям, но только зарегистрированные пользователи могут создавать новые и редактировать существующие сообщения. В представлении, в котором пользователи редактируют сообщения, перед обработкой формы убедитесь в том, что редактируемое сообщение принадлежит именно этому пользователю.

     

    Итоги

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

    После создания простой системы аутентификации и регистрации пользователей вы ограничили доступ пользователей к некоторым страницам; для этого использовался декоратор @login_required. Затем данные были связаны с конкретными пользователями при помощи отношения внешнего ключа. Вы также узнали, как выполнить миграцию базы данных, когда миграция требует ввести данные по умолчанию.

    В последней части главы вы узнали, как ограничить состав данных, просматриваемых пользователем, с использованием функций представления. Для чтения соответствующих данных использовался метод filter(), а владелец запрашиваемых данных сравнивался с текущим пользователем.

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

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