Создание вашего первого приложения Django, часть 4
Этот учебник начинается там, где Учебник 3 закончился. Мы продолжаем приложение Web-опроса и сосредоточимся на обработке простых форм и сокращении кода.
Создайте простую форму
Обновим шаблон деталей опроса (“polls/detail.html”) из последнего учебника, чтобы шаблон содержал элемент HTML <form>.
<h1>{{ question.question_text }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% 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 %}
<input type="submit" value="Vote" />
</form>
Краткий обзор:
- Вышеприведённый шаблон отображает переключатель выбора для каждого варианта ответа. Значение
valueкаждого переключателя выбора — это идентификатор соответствующего варианта ответа. Значениеnameкаждого переключателя выбора —"choice". Это означает, что когда кто-то выбирает один из переключателей выбора и отправляет форму, он отправит данные POSTchoice=#, где # — идентификатор выбранного варианта. Это базовый принцип HTML-форм. - Мы установили
actionформы в{% url 'polls:vote' question.id %}, и мы установилиmethod="post". Использованиеmethod="post"(вместоmethod="get") очень важно, потому что действие отправки этой формы изменит данные на стороне сервера. Всякий раз, когда вы создаёте форму, изменяющую данные на стороне сервера, используйтеmethod="post". Этот совет не специфичен для Django; это просто хорошая практика веб-разработки. forloop.counterуказывает, сколько раз тэгforпрошёл по своему циклу.- Поскольку мы создаём форму POST (которая может повлиять на изменение данных), нам нужно беспокоиться о межсайтовых фальсификациях запросов. К счастью, вам не нужно слишком сильно беспокоиться, потому что Django поставляется с очень простым в использовании механизмом защиты от них. Короче говоря, все формы POST, направленные на внутренние URL-адреса, должны использовать тэг шаблона
{% csrf_token %}.
Теперь создадим Django-представление, которое обрабатывает отправленные данные и что-то с ними делает. Помните, в Учебнике 3 мы создали URLconf для приложения polls, которое включает эту строку:
path('<int:question_id>/vote/', views.vote, name='vote'),
Мы также создали дублированное реализацию функции vote(). Давайте создадим реальную версию. Добавьте следующее в 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 см. документацию по запросам и ответам.
После того, как кто-то проголосовал за вопрос, представление vote() перенаправляет на страницу результатов для этого вопроса. Давайте напишем это представление:
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:
<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/ в вашем браузере и проголосуйте за вопрос. Вы должны увидеть страницу результатов, которая обновляется каждый раз, когда вы голосуете. Если вы отправите форму, не выбрав вариант, вы увидите сообщение об ошибке.
Примечание
Код нашего представления vote() имеет небольшую проблему. Он сначала получает объект selected_choice из базы данных, затем вычисляет новое значение votes, а затем сохраняет его обратно в базу данных. Если два пользователя вашего сайта попытаются проголосовать в совершенно одинаковое время, это может пойти не так: одно и то же значение, например 42, будет получено для votes. Затем для обоих пользователей вычисляется и сохраняется новое значение 43, но ожидаемым значением должно быть 44.
Это называется гоночной ситуацией. Если вас интересует, вы можете прочитать Избегание гоночных ситуаций с использованием F(), чтобы узнать, как можно решить эту проблему.
Использование универсальных представлений: меньше кода — лучше
Представления detail() (из Учебника 3) и results() очень просты — и, как упоминалось выше, избыточны. Представление index(), которое отображает список опросов, аналогично.
Эти представления представляют собой распространённый случай базовой веб-разработки: получение данных из базы данных в соответствии с параметром, переданным в URL, загрузка шаблона и возвращение отрендеренного шаблона. Поскольку это так часто встречается, Django предоставляет сокращение, называемое системой «универсальных представлений».
Универсальные представления абстрагируют общие шаблоны до такой степени, что вам даже не нужно писать код Python, чтобы создать приложение.
Давайте переведём наше приложение poll на систему универсальных представлений, чтобы мы могли удалить кучу своего кода. Мы должны будем выполнить несколько шагов для конвертации:
- Преобразовать URLconf.
- Удалить некоторые старые, не нужные представления.
- Ввести новые представления на основе универсальных представлений Django.
Подробнее см. ниже.
Почему перестановка кода?
Как правило, при написании приложения Django вы оцениваете, подходят ли вам универсальные представления, и используете их с самого начала, а не переписываете свой код наполовину. Но этот учебник намеренно сосредоточился на написании представлений «трудным способом» до сих пор, чтобы сосредоточиться на основных концепциях.
Вы должны знать основы математики, прежде чем использовать калькулятор.
Изменить URLconf
Сначала откройте polls/urls.py URLconf и измените его следующим образом:
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 и измените его следующим образом:
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 использует шаблон с именем <app name>/<model name>_detail.html. В нашем случае он будет использовать шаблон "polls/question_detail.html". Атрибут template_name используется для указания Django использовать имя конкретного шаблона вместо автоматически сгенерированного имени шаблона по умолчанию. Мы также указываем template_name для представления списка results — это гарантирует, что представления результатов и представления деталей имеют различный вид при рендеринге, хотя оба они являются DetailView на уровне реализации.
Аналогично, обобщенное представление ListView использует шаблон по умолчанию с именем <app name>/<model
name>_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 этого руководства, чтобы узнать о тестировании нашего приложения опросов.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/2.0/intro/tutorial04/