Spec-Zone.ru › Django 3.1

Написание вашего первого приложения Django, часть 4

Этот учебник начинается там, где Учебник 3 закончился. Мы продолжаем приложение Web-poll и сосредоточимся на обработке форм и сокращении кода.

Где получить помощь:

Если у вас возникли трудности с прохождением этого учебника, обратитесь к разделу Получение помощи раздела вопросов и ответов.

Напишите минимальную форму

Обновим шаблон подробностей опроса («polls/detail.html») из последнего учебника, чтобы шаблон содержал HTML-элемент:

polls/templates/polls/detail.html
<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>

Краткое описание:

  • Вышеупомянутый шаблон отображает переключатель радио для каждого варианта ответа. Идентификатор каждого переключателя радио — это ID соответствующего варианта ответа. Значение каждого переключателя радио — "choice". Это означает, что когда кто-то выбирает один из переключателей радио и отправляет форму, он отправит данные POST choice=# , где # — 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.py
path('<int:question_id>/vote/', views.vote, name='vote'),

Мы также создали фиктивное реализацию функции vote(). Давайте создадим реальную версию. Добавьте следующее в polls/views.py:

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-комментарии выше, после успешной обработки данных POST всегда следует возвращать HttpResponseRedirect. Этот совет не специфичен для Django; это хорошая практика веб-разработки в целом.

  • Мы используем функцию reverse() в конструкторе HttpResponseRedirect в этом примере. Эта функция помогает избежать необходимости жёстко задавать URL-адрес в функции представления. Ей передаётся имя представления, которому мы хотим передать управление, и переменная часть шаблона URL, указывающая на это представление. В данном случае с помощью URLconf, который мы настроили в Учебнике 3, этот вызов reverse() вернёт строку, подобную

    '/polls/3/results/'
    

    где 3 — значение question.id. Этот URL-адрес перенаправления затем вызовет представление 'results' для отображения конечной страницы.

Как упоминалось в Учебнике 3, request является объектом HttpRequest. Для получения дополнительной информации об объектах HttpRequest см. документацию по запросам и ответам.

После того, как кто-то проголосует за вопрос, представление vote() перенаправляет на страницу результатов для этого вопроса. Давайте напишем это представление:

polls/views.py
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:

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 на использование системы универсальных представлений, чтобы удалить кучу собственного кода. Нам нужно будет выполнить несколько шагов для преобразования. Мы будем:

  1. Преобразовать URLconf.
  2. Удалить некоторые старые, ненужные представления.
  3. Ввести новые представления на основе универсальных представлений Django.

Подробнее см. ниже.

Почему этот перенос кода?

Как правило, при написании приложения Django вы оцениваете, подходят ли вам универсальные представления, и используете их с самого начала, а не переписываете код на полпути. Но этот учебник намеренно сосредоточился на написании представлений «сложным способом» до сих пор, чтобы сосредоточиться на основных понятиях.

Вы должны знать арифметику до того, как начнете использовать калькулятор.

Изменение URLconf

Сначала откройте файл polls/urls.py 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 и измените его следующим образом:

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/3.1/intro/tutorial04/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API