Создание первого приложения на Django, часть 4

Эта часть учебника продолжает Часть 3. Мы продолжаем разработку приложения Web-poll и сосредоточимся на простой обработке форм и сокращении нашего кода.

Где получить помощь:

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

Пишем минимальную форму

Давайте обновим наш детальный шаблон опроса («polls/detail.html») из последнего урока, чтобы шаблон содержал HTML-элемент <form>:

polls/templates/polls/detail.html
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

Краткое изложение:

  • Приведенный выше шаблон отображает переключатель для каждого вопроса. value каждого переключателя является идентификатором (ID) соответствующего вопроса. name каждой радиокнопки - это "choice". Это означает, что когда кто-то выбирает один из переключателей и отправляет форму, он отправляет POST-данные choice=# где # - идентификатор выбранного варианта. Это основная концепция форм HTML.
  • Мы устанавливаем action формы в {% url 'polls: voice' question.id %} и устанавливаем method="post". Использование method="post" ` отличие от ``method="get") очень важно, потому что отправка этой формы изменит данные на стороне сервера. Всякий раз, когда вы создаете форму, которая изменяет данные на стороне сервера, используйте method="post". Этот совет не относится только к Django; Это хорошая практика веб-разработки в целом.
  • forloop.counter указывает, сколько раз тег for прошел цикл
  • Поскольку мы создаем форму POST (которая может повлиять на изменение данных), нам нужно беспокоиться о подделках межсайтовых запросов. К счастью, вам не нужно слишком сильно беспокоиться, потому что Django поставляется с полезной системой для защиты от него. Короче говоря, все формы POST, предназначенные для внутренних URL-адресов, должны использовать тег шаблона {% csrf_token %}.

Теперь давайте создадим представление Django, которое обрабатывает отправленные данные и что-то с ними делает. Помните, в Части 3 мы создали URLconf для приложения polls, включающего следующую строку:

polls/urls.py
path('<int:question_id>/vote/', views.vote, name='vote'),

Мы также создали фиктивную реализацию функции vote(). Давайте создадим настоящую версию. Добавьте следующее в polls/views.py:

polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

Этот код включает в себя несколько моментов, которые мы еще не рассмотрели в этом руководстве:

  • request.POST представляет собой объект, подобный словарю, который позволяет получить доступ к отправленным данным по ключу. В этом случае request.POST ['choice'] возвращает идентификатор выбранного варианта в виде строки. Значения request.POST всегда являются строками.

    Обратите внимание, что Django также предоставляет request.GET для доступа к данным GET таким же образом, но мы явно используем request.POST в нашем коде, чтобы гарантировать, что данные изменяются только через вызов POST.

  • request.POST ['choice'] будет вызывать KeyError, если ключ choice не был предоставлен в POST. Приведенный выше код проверяет KeyError и повторно отображает форму вопроса с сообщением об ошибке, если choice не задано.

  • После увеличения счетчика выбора код возвращает HttpResponseRedirect, а не обычный HttpResponse. HttpResponseRedirect принимает один аргумент: URL-адрес, на который будет перенаправлен пользователь (см. следующий пункт о том, как мы создаем URL-адрес в этом случае).

    Как указано выше в комментарии Python, вы должны всегда возвращать HttpResponseRedirect после успешной обработки данных POST. Этот совет не относится только к Django; Это хорошая практика веб-разработки в целом.

  • В этом примере мы используем функцию reverse() в конструкторе HttpResponseRedirect. Эта функция помогает избежать жесткого кодирования URL-адреса в функции представления. Ему дается имя представления, которому мы хотим передать управление, и переменная часть шаблона URL, которая указывает на это представление. В этом случае, используя URLconf, который мы настроили в Части 3, вызов reverse() вернет строку, подобную

    '/polls/3/results/'
    

    где 3 - это значение question.id. Этот перенаправленный URL-адрес затем вызовет представление 'results', чтобы отобразить последнюю страницу.

Как уже упоминалось в Части 3, request является объектом HttpRequest. Подробнее об объектах HttpRequest см. Документацию запрос и ответ.

После того, как кто-то проголосовал в опросе, представление voice() перенаправляет на страницу результатов для опроса. Давайте напишем это представление:

polls/views.py
from django.shortcuts import get_object_or_404, render


def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

Оно почти такое же, как представление detail() из Части 3. Разница лишь в названии шаблона. Мы исправим эту избыточность позже.

Теперь создадим шаблон polls/results.html:

polls/templates/polls/results.html
<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

Теперь перейдите к /polls/1/ в вашем браузере и проголосуйте в опросе. Вы должны увидеть страницу результатов, которая обновляется каждый раз, когда вы голосуете. Если вы отправляете форму, не выбрав вариант, вы должны увидеть сообщение об ошибке.

Примечание

Код для нашего представления poll() содержит небольшую проблему. Сначала он получает объект selected_choice из базы данных, затем вычисляет новое значение voice и затем сохраняет его обратно в базу данных. Если два пользователя сайта попытаются проголосовать в одно и то же время, это может вызвать ошибку: одно и то же значение, скажем, 42, будет получено для votes. Затем для обоих пользователей новое значение 43 вычисляется и сохраняется, ноожидаемым значением будет 44.

Это называется состояние гонки. Если вам интересно, вы можете почитать Избегание условий гонки с помощью F(), чтобы узнать, как можно решить эту проблему.

Используйте базовые представления: чем меньше кода, тем лучше

Представления detail() (из Части 3) и results() очень короткие и, как упоминалось выше, избыточны. Представление index(), которое отображает список опросов, аналогично.

Эти представления представляют собой типичный случай базовой веб-разработки: получение данных из базы в соответствии с параметром, переданным в URL, загрузка шаблона и возврат обработанного шаблона. Поскольку это часто встречается, Django предоставляет дополнения, называемые системой «базовых представлений».

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

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

  1. Преобразование URLconf.
  2. Удаление некоторых старых, ненужных представлений.
  3. Введите новые представления, основанные на базовых представлениях Django.

Продолжайте чтение для ознакомления с деталями.

Почему код перемешался?

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

Вы должны знать основы математики, прежде чем начнете использовать калькулятор.

Изменение URLconf

Сначала откройте URLconf polls/urls.py и измените его следующим образом:

polls/urls.py
from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

Обратите внимание, что имя сопоставленного шаблона в строках пути второго и третьего шаблонов изменилось с <question_id> на <pk>.

Изменение представления

Далее мы собираемся удалить наши старые представления index, detail и results и использовать вместо них базовые представления Django. Для этого откройте файл polls/views.py и измените его следующим образом:

polls/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.

Здесь мы используем два базовых представления: ListView и DetailView. Соответственно, эти два представления абстрагируют понятия «отображать список объектов» и «отображать страницу подробностей для определенного типа объекта».

  • Каждое базовое представление должно знать, на какую модель оно будет использовать. Это обеспечивается с помощью атрибута model.
  • Общее представление DetailView ожидает, что значение первичного ключа, полученное из URL, будет называться "pk", поэтому мы изменили question_id на pk для базовых представлений.

По умолчанию базовое представление DetailView использует шаблон с именем <имя приложения>/<имя модели>_detail.html. В нашем случае он будет использовать шаблон "polls/question_detail.html". Атрибут template_name используется для указания Django использовать определенное имя шаблона вместо автоматически сгенерированного по умолчанию. Мы также указываем template_name для представления списка results - это гарантирует, что представление результатов и представление подробностей при визуализации будут выглядеть по-разному, даже если они оба DetailView.

Аналогично, базовое представление ListView использует шаблон по умолчанию с именем <имя приложения>/<имя модели>_list.html; мы используем template_name, чтобы сказать ListView использовать наш существующий шаблон "polls/index.html".

В предыдущих частях руководства шаблоны были снабжены контекстом, который содержит переменные контекста question и latest_question_list. Для DetailView переменная question предоставляется автоматически - поскольку мы используем модель Django (Question), Django может определить подходящее имя для переменной контекста. Однако для ListView автоматически генерируемой переменной контекста является question_list. Чтобы переопределить это, мы предоставляем атрибут context_object_name, определяющий, что мы хотим использовать latest_question_list. В качестве альтернативного подхода вы можете изменить свои шаблоны, чтобы они соответствовали новым переменным контекста по умолчанию, но гораздо проще заставить Django использовать нужную переменную.

Запустите сервер и используйте новое приложение опроса на основе базовых представлений.

Для получения полной информации смотрите документацию базовых представлений.

Если вы знакомы с формами и базовыми представлениями, прочитайте часть 5 этого руководства, чтобы узнать о тестировании нашего приложения для опросов.

Вернуться на верх