Написание вашей первой Django-приложения, часть 4
Этот учебник продолжается там, где Учебник 3 оборвался. Мы продолжаем приложение Web-poll и сосредоточимся на простой обработке форм и сокращении кода.
Напишите простую форму
Обновим шаблон детали опроса («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каждой радиокнопки — это ID соответствующего варианта ответа.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 для приложения опросов, который включает эту строку:
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
Мы также создали демонстрационную реализацию функции vote(). Давайте создадим реальную версию. Добавьте следующее в polls/views.py:
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
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 выше, после успешной обработки данных POST всегда следует возвращать
HttpResponseRedirect. Этот совет не специфичен для 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, а затем сохраняет его обратно в базу данных. Если два пользователя вашего сайта попытаются проголосовать в *точно одно и то же время*, это может пойти не так: для votes будет получено одно и то же значение, например 42. Затем для обоих пользователей вычисляется новое значение 43 и сохраняется, но ожидаемое значение должно быть 44.
Это называется *проблемой гонки*. Если вас интересует, вы можете прочитать Избегание гонок с использованием F(), чтобы узнать, как решить эту проблему.
Использование обобщенных представлений: меньше кода — лучше
Представления detail() (из Учебника 3) и results() очень простые — и, как упоминалось выше, избыточные. Представление index(), отображающее список опросов, аналогично.
Эти представления представляют собой распространенный случай базовой разработки веб-приложений: получение данных из базы данных в соответствии с параметром, переданным в URL, загрузка шаблона и возвращение рендеренного шаблона. Поскольку это так распространено, Django предоставляет сокращение, называемое системой «обобщенных представлений».
Обобщенные представления абстрагируют общие паттерны до такой степени, что вам даже не нужно писать код Python, чтобы написать приложение.
Давайте переведем наше приложение опросов на использование системы обобщенных представлений, чтобы мы могли удалить кучу нашего собственного кода. Нам нужно будет сделать несколько шагов, чтобы осуществить перевод:
- Преобразовать URLconf.
- Удалить некоторые старые, ненужные представления.
- Ввести новые представления, основанные на обобщенных представлениях Django.
Подробности см. ниже.
Почему перестановка кода?
Как правило, при написании Django-приложения вы оцените, подходят ли обобщенные представления для вашей задачи, и будете использовать их с самого начала, а не переписывать код на полпути. Но этот учебник намеренно фокусировался на написании представлений «трудным способом» до сих пор, чтобы сосредоточиться на основных концепциях.
Вы должны знать основы математики, прежде чем начать пользоваться калькулятором.
Изменить URLconf
Сначала откройте polls/urls.py URLconf и измените его следующим образом:
from django.conf.urls import url
from . import views
app_name = 'polls'
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
Обратите внимание, что имя сопоставленного шаблона в регулярных выражениях второго и третьего шаблонов изменилось с <question_id> на <pk>.
Изменить представления
Далее мы удалим наши старые представления index, detail, и results и вместо них будем использовать обобщенные представления Django. Для этого откройте файл polls/views.py и измените его следующим образом:
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
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/1.11/intro/tutorial04/