Создание первого приложения на Django, часть 4¶
Эта часть учебника продолжает Часть 3. Мы продолжаем разработку приложения Web-poll и сосредоточимся на простой обработке форм и сокращении нашего кода.
Где получить помощь:
Если у вас возникли проблемы с просмотром этого учебника, перейдите в раздел Получение справки FAQ.
Пишем минимальную форму¶
Давайте обновим наш детальный шаблон опроса («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"
. Это означает, что когда кто-то выбирает один из переключателей и отправляет форму, он отправляет 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, включающего следующую строку:
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
см. Документацию запрос и ответ.
После того, как кто-то проголосовал в опросе, представление voice()
перенаправляет на страницу результатов для опроса. Давайте напишем это представление:
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/
в вашем браузере и проголосуйте в опросе. Вы должны увидеть страницу результатов, которая обновляется каждый раз, когда вы голосуете. Если вы отправляете форму, не выбрав вариант, вы должны увидеть сообщение об ошибке.
Примечание
Код для нашего представления poll()
содержит небольшую проблему. Сначала он получает объект selected_choice
из базы данных, затем вычисляет новое значение voice
и затем сохраняет его обратно в базу данных. Если два пользователя сайта попытаются проголосовать в одно и то же время, это может вызвать ошибку: одно и то же значение, скажем, 42, будет получено для votes
. Затем для обоих пользователей новое значение 43 вычисляется и сохраняется, ноожидаемым значением будет 44.
Это называется состояние гонки. Если вам интересно, вы можете почитать Избегание условий гонки с помощью F(), чтобы узнать, как можно решить эту проблему.
Используйте базовые представления: чем меньше кода, тем лучше¶
Представления detail()
(из Части 3) и results()
очень короткие и, как упоминалось выше, избыточны. Представление index()
, которое отображает список опросов, аналогично.
Эти представления представляют собой типичный случай базовой веб-разработки: получение данных из базы в соответствии с параметром, переданным в URL, загрузка шаблона и возврат обработанного шаблона. Поскольку это часто встречается, Django предоставляет дополнения, называемые системой «базовых представлений».
Базовые представления абстрагируют общие шаблоны до такой степени, что вам даже не нужно писать код Python для написания приложения.
Давайте преобразуем наше приложение для опроса, чтобы использовать систему общих представлений, чтобы мы могли удалить часть нашего собственного кода. Мы должны сделать несколько шагов, чтобы сделать преобразование. Мы будем:
- Преобразование URLconf.
- Удаление некоторых старых, ненужных представлений.
- Введите новые представления, основанные на базовых представлениях Django.
Продолжайте чтение для ознакомления с деталями.
Почему код перемешался?
Как правило, при написании приложения Django вы будете оценивать, подходят ли базовые представления для вашей задачи, и будете использовать их с самого начала, а не заниматься рефакторингом своего кода на полпути. Но этот урок намеренно сфокусирован на написании представлений «трудным путем», чтобы сосредоточиться на основных понятиях.
Вы должны знать основы математики, прежде чем начнете использовать калькулятор.
Изменение URLconf¶
Сначала откройте URLconf 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
и измените его следующим образом:
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 этого руководства, чтобы узнать о тестировании нашего приложения для опросов.