Написание вашей первой Django-приложения, часть 4
Этот учебник начинается там, где Урок 3 закончил. Мы продолжаем приложение веб-опроса и сосредоточимся на обработке формы и сокращении кода.
Где получить помощь:
Если у вас возникли трудности с прохождением этого учебника, пожалуйста, перейдите к разделу Получение помощи раздела FAQ.
Напишите минимальную форму
Обновим шаблон подробностей опроса («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каждой радиокнопки.nameкаждой радиокнопки —"choice". Это означает, что когда кто-то выбирает одну из радиокнопок и отправляет форму, это отправит данные POSTchoice=#, где # — ID выбранного варианта. Это базовый принцип 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, который включает эту строку:
polls/urls.pypath('<int:question_id>/vote/', views.vote, name='vote'),
Мы также создали фиктивную реализацию функции vote(). Давайте создадим реальную версию. Добавьте следующее в polls/views.py;
polls/views.pyfrom 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']возвращает ID выбранного варианта ответа в виде строки. Значения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() перенаправляет на страницу результатов для вопроса. Давайте напишем это представление:
polls/views.pyfrom 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/ в вашем браузере и проголосуйте за вопрос. Вы должны увидеть страницу результатов, которая обновляется каждый раз, когда вы голосуете. Если вы отправите форму, не выбрав вариант, вы увидите сообщение об ошибке.
Примечание
Код нашего представления vote() имеет небольшую проблему. Он сначала получает объект selected_choice из базы данных, затем вычисляет новое значение votes, а затем сохраняет его обратно в базу данных. Если два пользователя вашего сайта пытаются проголосовать в то же самое время, это может пойти не так: одинаковое значение, скажем 42, будет извлечено для votes. Затем, для обоих пользователей вычисляется и сохраняется новое значение 43, но ожидаемым значением должно было быть 44.
Это называется гоночной ситуацией. Если вас интересует эта тема, вы можете прочитать Избежание гоночных ситуаций с использованием F(), чтобы узнать, как решить эту проблему.
Использование обобщенных представлений: меньше кода — лучше
Представления detail() (из Урока 3) и results() очень короткие — и, как упоминалось выше, избыточные. Представление index(), которое отображает список опросов, аналогично.
Эти представления представляют собой распространённый случай в веб-разработке: получение данных из базы данных в соответствии с параметром, переданным в URL-адресе, загрузка шаблона и возвращение рендеренного шаблона. Поскольку это так распространено, Django предоставляет сокращение, называемое системой «обобщённых представлений».
Обобщенные представления абстрагируют общие шаблоны до такой степени, что вам даже не нужно писать Python-код для написания приложения.
Давайте переведём наше приложение polls на использование системы обобщённых представлений, чтобы мы могли удалить кучу нашего кода. Нам придётся выполнить несколько шагов для преобразования.
- Преобразовать URLconf.
- Удалить некоторые старые, ненужные представления.
- Ввести новые представления, основанные на обобщенных представлениях Django.
Подробнее см. ниже.
Почему перестановка кода?
В целом, при написании Django-приложения вы оцените, подходят ли обобщённые представления для вашей проблемы, и будете использовать их с самого начала, а не перестраивать свой код на полпути. Но этот учебник намеренно сосредотачивался на написании представлений «сложными методами» до сих пор, чтобы сфокусироваться на основных концепциях.
Вы должны знать основы математики, прежде чем начать использовать калькулятор.
Изменить URLconf
Сначала откройте polls/urls.py URLconf и измените его следующим образом:
polls/urls.pyfrom 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.pyfrom 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/4.1/intro/tutorial04/