Django: пример создания приложения — часть 5: создание форм и общие представления (generic views)

Автор: | 05/16/2015
 

django_logo_2

Предыдущая часть

Пишем простую форму

Давайте обновим шаблон деталей вопроса в нашем приложении голосования (файл 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". Это значит, что когда кто-то выбирает один из вариантов, и подтверждает действие (type=submit) — форма отправляет POST-запрос с данными choice=#, где # — это ID выбранного choice. Это базовая концепция HTML-форм.
  • мы установили action формы {% url 'polls:vote' question.id %}, и метод POST. Использование метода POST (в отличи от метода GET) очень важно, так как действие подтверждения формы затрагивает сервер. Каждый раз, когда вы создаёте форму, которая имеет какое-то влияние на сервер — используйте method="post". Это касается не только Django — а вообще хорошая практика в веб-разработке.
  • forloop.counter подсчитывает, сколько раз сработал тег for;
  • так как мы создаём форму POST (которая выполняет модификацию данных) — нам необходимо побеспокоиться о Межсайтовой Подделке Запросов (Cross Site Request Forgeries, XSRF). К счастью — нам не надо переживать об этом сильно много, так как Django имеет встроенную систему защиты от неё. Вкратце — все формы с POST, которые направлены на внутренние URL-ы должны использовать тег {% csrf_token %}.

Теперь — давайте создадим представление, которое будет обрабатывать переданные данные, и что-то с ними делать. В предыдущей части мы уже создали URLconf для нашего приложения polls, который включает в себя такую строку:

url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),

Так же — мы создали простую функцию vote(). Давайте её приведём к более полезному виду:

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse

from .models import Choice, Question
# ...
def vote(request, question_id):
    p = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = p.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # отобразить форму голосования заново
        return render(request, 'polls/detail.html', {
            'question': p,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # всегда выполняйте возврат HttpResponseRedirect после успешной обработки POST
        # Это предотвращает двойное использование данных
        # если пользователь нажимает кнопку Назад
        return HttpResponseRedirect(reverse('polls:results', args=(p.id,)))

В этом коде встречаются вещи, с которыми мы ещё не встречались:

  • request.POST — это объект-словарь, который позволяет вам обращаться к переданным после submit данным по имени:ключу. В данном случае — request.POST['choice'] возвращает ID выбранного голоса в виде строки; помните, что Django также предоставляет request.GET для обращения к данным GET;
  • request.POST['choice'] вызовет KeyError, если в данных POST не было choice; код выше проверяет на предмет KeyError и отображает форму голосования заново с сообщением об ошибке, если choice не был передан;
  • после увеличения счётчика голосов — код возвращает HttpResponseRedirect, а не обычный HttpResponse. HttpResponseRedirect принимает один аргумент — URL, на который пользователь будет перенаправлен (смотрите ниже — как именно мы сконструировали URL в данном случае);
  • мы используем функцию reverse() в конструкторе HttpResponseRedirect; эта функция помогает избежать использования URL-ов, вписанных непосредственно в код представления; ему передаётся имя функции, которой мы передаём управление, а часть переменной шаблона URL-а, который указывает на это представление; в данном случае — используя URLconf, который мы настроили в предыдущей части, вызов reverse() вернёт строку вида ‘/polls/3/results/‘, где «3» — это значение p.id; этот вызов переадресации вызовет представление result(), что бы отобразить результаты

Как уже упоминалось в четвёртой части — request это объект HttpRequest. Больше информации о HttpRequest можно получить на странице request and response documentation.

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

Давайте перепишем представление results() так:

...
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() из предыдущей части. Разница только в названии файла шаблона.

Далее — создайте шаблон 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/ и проголосуйте в вашем вопросе. Вы должны увидеть страницу с результатами голосования. Если вы нажмёте на «Голосовать» без выбора варианта ответа — должна отобразиться ошибка:

django_21

django_22

Использование общих представлений: меньше кода — лучше

Функции detail() и result() очень простые и избыточные. То же касается и функции index(), которая отображает список вопросов.

Эти представления являются распространёнными случаем в веб-разработке: получение данных из базы данных в соответствии с параметром, переданным в URL-е, загрузка шаблона и возврат его отрендеренного варианта. Так как эти задачи часто используются — Django предоставляет систему, которая называется «общие представления» (generic views).

Общие представления являются некими шаблонами, при использовании которых вам иногда можно вообще не писать код на Python.

Давайте изменим наше приложение-голосование на использование таких общих шаблонов — так мы сможем удалить значительную часть кода.

Для этого нам потребуется выполнить всего несколько шагов. Наши действия:

  1. изменим URLconf;
  2. удалим некоторые старые, уже не нужные, представления;
  3. добавим новые представления, основанные на общих представлениях Django.

Меняем URLconf

Для начала — откройте файл polls/urls.py, и измените URLconf так:

from django.conf.urls import url

from . import views

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.core.urlresolvers 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):
    # оставьте без изменений

 

Тут мы использовали два общих представления — 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 — так мы можем быть уверены, что представления results и detail  будут использовать различный вид после рендеринга.

Аналогично представление 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.

Запустите ваш сервер разработки — и используйте ваше новое приложение, основанное для общих шаблонах.