Написание вашего первого приложения Django, часть 4
Этот учебник начинается там, где закончился Учебник 3. Мы продолжаем работу с приложением web-опроса и сосредоточимся на обработке форм и сокращении кода.
Где получить помощь:
Если у вас возникли трудности с этим учебником, пожалуйста, перейдите к разделу Получение помощи раздела 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>
Краткое описание:
- Вышеуказанный шаблон отображает радиокнопку для каждого варианта ответа на вопрос. Идентификатор каждой радиокнопки — это идентификатор соответствующего варианта ответа на вопрос. Значение каждой радиокнопки —
"choice". Это означает, что когда кто-то выбирает одну из радиокнопок и отправляет форму, она отправит данные POSTchoice=#, где # — ID выбранного варианта. Это базовая концепция HTML-форм. - Мы установили метод отправки формы на
{% 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/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, чтобы написать приложение. Например, обобщенные представления ListView и DetailView абстрагируют понятия «отображение списка объектов» и «отображение страницы деталей для конкретного типа объекта» соответственно.
Давайте переведем наше приложение опросов на использование системы обобщенных представлений, чтобы мы могли удалить кучу собственного кода. Нам нужно будет сделать несколько шагов для преобразования.
- Преобразовать URLconf.
- Удалить некоторые старые, ненужные представления.
- Ввести новые представления на основе обобщенных представлений Django.
Подробности см. ниже.
END_OF_DOCUMENT_MARKER ```Почему перестановка кода?
Как правило, при написании приложения 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>. Это необходимо, так как мы будем использовать обобщенное представление DetailView для замены наших detail() и results() представлений, и оно ожидает, что значение первичного ключа, полученное из URL, будет названо "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.
...
Каждое обобщенное представление должно знать, к какой модели оно будет применяться. Это обеспечивается атрибутом model (в этом примере, model =
Question для DetailView и ResultsView) или путем определения метода get_queryset() (как показано в IndexView).
По умолчанию обобщенное представление 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/5.0/intro/tutorial04/