Написание вашей первой 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 для приложения 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']возвращает 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() перенаправляет на страницу результатов для вопроса. Давайте напишем это представление:
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
Сначала откройте 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 использует шаблон, называемый <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.2/intro/tutorial04/