Изучаем Python — страница 51 из 61


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

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

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

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

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

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