Мастер форм

Django поставляется с приложением «мастер форм», которое распределяет формы по нескольким страницам сайта. Приложение хранит своё состояние в одном из бэкэндов, таким образом обработка введённой информации может быть отложена до заполнения последней формы.

Вам может потребоваться это приложение для отображения длинной формы, которая является слишком громоздкой для отображения на одной странице. Первая страница может запрашивать основную информацию, а вторая страница может спрашивать дополнительную информацию и так далее.

Термин «мастер» в этом контексте описан на Википедии.

Как это работает

Рассмотрим основные шаги использования мастера пользователем:

  1. Пользователь открывает первую страницу мастера, заполняет форму и отправляет её.

  2. Сервер проверяет данные. Если они содержат ошибку, то форма отображается заново, показывая ошибки. Если данные прошли проверку, сервер сохраняет текущее состояние мастера в бэкэнде и перенаправляет пользователя на следующую страницу.

  3. Оба шага повторяются для каждой формы мастера.

  4. После того, как пользователь заполнит все формы и все данные пройдут проверку, мастер обрабатывает данные, сохраняя их в базе данных, отправляя почту или производит другие требуемые действия.

Использование

Это приложение максимально автоматизирует данный процесс. В общем случае, вам надо сделать следующее:

  1. Определить ряд классов Form, по одному на каждую страницу мастера.

  2. Создать наследника класса WizardView, который будет определять , что надо сделать когда все формы будут заполнены и проверены. Класс также позволяет изменить поведение мастера.

  3. Создать несколько шаблонов, которые будут отображать формы. Вы можете определить либо один общий шаблон для обработки каждой формы, либо создать собственный шаблон для каждой формы.

  4. Добавить django.contrib.formtools в параметр конфигурации INSTALLED_APPS.

  5. Зарегистрировать URL, который будет вызывать метод as_view() вашего класса WizardView.

Определение классов Form

Первым шагом создания мастера форм будет определение классов Form. Это обычные классы django.forms.Form, описанные в соответствующем разделе. Код этих классов может располагаться в любом месте вашего проекта, но по принятым соглашениям лучше его располагать в файле forms.py вашего приложения.

Давайте для примера напишем мастер для «контактной формы», где на первой странице будем запрашивать адрес электронной почты и тему, а на второй — само тело сообщения. Вот как файл forms.py может выглядеть:

from django import forms

class ContactForm1(forms.Form):
    subject = forms.CharField(max_length=100)
    sender = forms.EmailField()

class ContactForm2(forms.Form):
    message = forms.CharField(widget=forms.Textarea)

Примечание

Для того, чтобы использовать класс FileField на любой форме, обратитесь к расположенному далее разделу :ref:` Обработка файлов <wizard-files>`.

Наследование WizardView

class SessionWizardView
class CookieWizardView

Следующим шагом будет создание класса django.contrib.formtools.wizard.views.WizardView. Вы можете также использовать классы SessionWizardView или CookieWizardView, которые используют соответствующий бэкэнд для хранения информации во время работы мастера (как видно по их именам, для хранения информации в сессии на сервере или в куках браузера).

Примечание

Для использования класса SessionWizardView следуйте инструкциям документации по сессиям для активации поддержки сессий.

Мы будем использовать класс SessionWizardView во всех примерах, но легко могли бы использовать и класс CookieWizardView. Как и в случае с классами Form, код класса WizardView может располагаться в любом месте проекта, но по соглашению его надо размещать в файле views.py.

Единственное требование к вашему классу — он должен реализовывать метод done().

WizardView.done(form_list)

Этот метод определяет, что должно происходить при отправке и проверке данных всех формы. Этот метод принимает список проверенных экземпляров класса Form.

Это упрощённый пример. Вместо выполнения всех операций с базой данных, метод просто заполняет шаблон проверенными данными:

from django.shortcuts import render_to_response
from django.contrib.formtools.wizard.views import SessionWizardView

class ContactWizard(SessionWizardView):
    def done(self, form_list, **kwargs):
        return render_to_response('done.html', {
            'form_data': [form.cleaned_data for form in form_list],
        })

Следует отметить, что этот метод будет выполняться через POST, поэтому по правилам хорошего тона после обработки следует перенаправлять пользователя на другую страницу. Вот другой пример:

from django.http import HttpResponseRedirect
from django.contrib.formtools.wizard.views import SessionWizardView

class ContactWizard(SessionWizardView):
    def done(self, form_list, **kwargs):
        do_something_with_the_form_data(form_list)
        return HttpResponseRedirect('/page-to-redirect-to-when-done/')

Обратитесь к разделу Дополнительные методы WizardView для получения информации по обработчикам класса WizardView.

Создание шаблонов для форм

Далее, необходимо создать шаблон для отображения форм мастера. По умолчанию каждая форма использует шаблон formtools/wizard/wizard_form.html. Вы можете изменить имя этого шаблона, переопределив либо атрибут template_name, либо метод get_template_names(), которые описаны в документации на класс TemplateResponseMixin. Последний класс позволяет использовать отдельные шаблоны для каждой формы (см. примеры далее).

Шаблон ожидает объект wizard, к которому привязаны различные элементы:

  • form – это экземпляр классов Form или BaseFormSet для текущего шага мастера (либо пустой, либо с ошибками).

  • steps – вспомогательный объект для доступа к данным других шагов мастера:

    • step0 – номер текущего шага (начинается с нуля).

    • step1 – номер текущего шага (начинается с единицы).

    • count – общее количество шагов.

    • first – признак первого шага.

    • last – признак последнего шага.

    • current – текущий (или первый) шаг.

    • next – следующий шаг.

    • prev – предыдущий шаг.

    • index – индекс текущего шага.

    • all – список всех шагов мастера.

Вы можете добавлять в контекст дополнительные переменные с помощью метода get_context_data() вашего потомка класса WizardView.

Вот полный пример шаблона:

{% extends "base.html" %}
{% load i18n %}

{% block head %}
{{ wizard.form.media }}
{% endblock %}

{% block content %}
<p>Step {{ wizard.steps.step1 }} of {{ wizard.steps.count }}</p>
<form action="" method="post">{% csrf_token %}
<table>
{{ wizard.management_form }}
{% if wizard.form.forms %}
    {{ wizard.form.management_form }}
    {% for form in wizard.form.forms %}
        {{ form }}
    {% endfor %}
{% else %}
    {{ wizard.form }}
{% endif %}
</table>
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.first }}">{% trans "first step" %}</button>
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}">{% trans "prev step" %}</button>
{% endif %}
<input type="submit" value="{% trans "submit" %}"/>
</form>
{% endblock %}

Примечание

Следует отметить, что тег {{ wizard.management_form }} должен обязательно присутствовать в шаблоне, иначе мастер будет работать неправильно.

Подключение мастера к URL

WizardView.as_view()

Наконец, нам надо указать какие формы надо использовать в мастере и затем подключить новый объект WizardView к URL в файле urls.py. Метод мастера as_view() принимает список экземпляров класса Form в качестве аргумента:

from django.conf.urls import patterns

from myapp.forms import ContactForm1, ContactForm2
from myapp.views import ContactWizard

urlpatterns = patterns('',
    (r'^contact/$', ContactWizard.as_view([ContactForm1, ContactForm2])),
)
Changed in Django 1.6.

Вы можете также передать список форм через атрибут класса form_list:

class ContactWizard(WizardView):
    form_list = [ContactForm1, ContactForm2]

Использование различных шаблонов для каждой формы

Как упоминалось ранее, вы можете указывать различные шаблоны для каждой формы. Рассмотрим пример использования мастера для реализации многошагового процесса покупки в интернет магазине. На первом шаге пользователь определяет адреса оплаты и доставки. На втором шаге. пользователь выбирает тип платежа. Если будет выбрана оплата с помощью кредитной карты, они введут информацию о карте на следующем шаге. На последнем шаге производится подтверждение покупки.

Вот так может выглядеть код подобного представления:

from django.http import HttpResponseRedirect
from django.contrib.formtools.wizard.views import SessionWizardView

FORMS = [("address", myapp.forms.AddressForm),
         ("paytype", myapp.forms.PaymentChoiceForm),
         ("cc", myapp.forms.CreditCardForm),
         ("confirmation", myapp.forms.OrderForm)]

TEMPLATES = {"address": "checkout/billingaddress.html",
             "paytype": "checkout/paymentmethod.html",
             "cc": "checkout/creditcard.html",
             "confirmation": "checkout/confirmation.html"}

def pay_by_credit_card(wizard):
    """Return true if user opts to pay by credit card"""
    # Get cleaned data from payment step
    cleaned_data = wizard.get_cleaned_data_for_step('paytype') or {'method': 'none'}
    # Return true if the user selected credit card
    return cleaned_data['method'] == 'cc'


class OrderWizard(SessionWizardView):
    def get_template_names(self):
        return [TEMPLATES[self.steps.current]]

    def done(self, form_list, **kwargs):
        do_something_with_the_form_data(form_list)
        return HttpResponseRedirect('/page-to-redirect-to-when-done/')
        ...

Файл urls.py будет содержать нечто подобное:

urlpatterns = patterns('',
    (r'^checkout/$', OrderWizard.as_view(FORMS, condition_dict={'cc': pay_by_credit_card})),
)
Changed in Django 1.6.

Словарь condiction_dict может быть передан как атрибут метода as_view()` или как атрибут класса  ``condition_dict:

class OrderWizard(WizardView):
    condition_dict = {'cc': pay_by_credit_card}

Следует отметить, что объект OrderWizard инициализируется списком пар. Первым элементом пары является строка с именем шага, а вторая – класс формы.

В данном примере метод get_template_names() возвращает список единый шаблон, который был выбран по имени текущего шага.

Дополнительные методы WizardView

class WizardView

Кроме метода done(), класс WizardView предоставляет несколько дополнительных методов (обработчиков), которые позволяют настроить поведение мастера.

Некоторые из этих методов принимают аргумент step, который является начинающимся с нуля строковым значением текущего шага мастера. (Т.е., первая форма — '0', а вторая — '1'.)

WizardView.get_form_prefix(step=None, form=None)

Возвращает префикс, который будет использоваться при вызове формы для переданного шага. step содержит имя шага, form – класс формы, который будет вызван с использованием возвращённого префикса.

Если step не передан, то будет определён автоматически. По умолчанию будет использован сам шаг, а параметр form использоваться не будет.

Для подробностей обратитесь к документации на префиксы форм.

WizardView.get_form_initial(step)

Возвращает словарь, который будет передан в качестве аргумента initial при создании экземпляра формы для шага step. Если во время создания мастера начальные данные не были предоставлены, то возвращается пустой словарь.

Стандартная реализация:

def get_form_initial(self, step):
    return self.initial_dict.get(step, {})
WizardView.get_form_kwargs(step)

Возвращает словарь, который будет использоваться в качестве именованных аргументов при создании экземпляра формы для шага step.

Стандартная реализация:

def get_form_kwargs(self, step):
    return {}
WizardView.get_form_instance(step)

Этот метод применяется только при использовании класса ModelForm для создания формы на шаге step.

Возвращает объект Model, который передаётся в качестве аргумента instance при создании модельной формы на шаге step. Если при создании мастера не был передан экземпляр модели, то возвращается None.

Стандартная реализация:

def get_form_instance(self, step):
    return self.instance_dict.get(step, None)
WizardView.get_context_data(form, **kwargs)

Возвращает шаблонный контекст для шага. Вы можете переопределить этот метод для добавления дополнительных данных на всех или на определённых шагах мастера. Этот метод возвращает словарь контекста для текущего шага.

Стандартные контекстные переменные шаблона:

  • Бэкэнд хранения сохраняет все дополнительные данные

  • form – экземпляр формы текущего шага.

  • wizard – экземпляр самого мастера.

Пример добавления дополнительных переменных для указанного шага:

def get_context_data(self, form, **kwargs):
    context = super(MyWizard, self).get_context_data(form=form, **kwargs)
    if self.steps.current == 'my_step_name':
        context.update({'another_var': True})
    return context
WizardView.get_prefix(*args, **kwargs)

Этот метод возвращает префикс, который использует хранилище. Бэкэнд использует префикс для хранения данных нескольких мастеров в одном хранилище. Это позволяет мастерам сохранять свои данные, не мешая друг другу.

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

Стандартная реализация:

def get_prefix(self, *args, **kwargs):
    # use the lowercase underscore version of the class name
    return normalize_name(self.__class__.__name__)
WizardView.get_form(step=None, data=None, files=None)

Этот метод создает форму для указанного шага step. Если шаг не указан, то текущий шаг определяется автоматически. Если вы переопределите get_form, вам необходимо указать step самостоятельно, используя self.steps.current как показано ниже. Метод принимает три аргумента:

  • step – шаг, для которого должен быть создан экземпляр формы.

  • data – передаётся в аргумент данных формы.

  • files – передаётся в аргумент файлов формы

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

Пример кода, который добавляет атрибут пользователя к форме на вопрос шаге.

def get_form(self, step=None, data=None, files=None):
    form = super(MyWizard, self).get_form(step, data, files)

    # determine the step if not given
    if step is None:
        step = self.steps.current

    if step == '1':
        form.user = self.request.user
    return form
WizardView.process_step(form)

Обработчик для изменения внутреннего состояния мастера, получающий полностью проверенный объект Form. Форма гарантированно получает нормализованные и проверенные данные.

Этот метод позволяет организовать обработку данных формы перед их помещением в хранилище. По умолчанию метод просто возвращает словарь form.data. Вы не должны изменять полученные данные, но можете дополнять их.

Следует отметить, что этот метод вызывается при каждом отображении страницы для всех выполненных шагов.

Стандартная реализация:

def process_step(self, form):
    return self.get_form_step_data(form)
WizardView.process_step_files(form)

Этот метод предоставляет возможность организовать обработку файлов формы перед их размещением в хранилище. По умолчанию метод просто возвращает словарь form.files. Вы не должны изменять полученные данные, но можете дополнять их.

Стандартная реализация:

def process_step_files(self, form):
    return self.get_form_step_files(form)
WizardView.render_goto_step(step, goto_step, **kwargs)
New in Django 1.6.

Этот метод вызывается когда шаг должен быть изменён на любой другой, кроме следующего. По-умолчанию, этот метод просто сохраняет запрошенный шаг goto_step в хранилище и затем отображает соответствующую форму.

Если требуется сохранить введённые данные текущего шага перед отображением следующего, вы можете переопределить этот метод.

WizardView.render_revalidation_failure(step, form, **kwargs)

Как только мастер пройдёт все шаги, он заново проверяет все формы, используя данные из хранилища.

Если хоть одна из форм не пройдёт проверку, то будет вызван этот метод. Метод принимает два аргумента, step и form.

Текущая реализация сбрасывает текущий шаг на первую из форм, которая не прошла проверку, и перенаправляет пользователя на неё.

Стандартная реализация:

def render_revalidation_failure(self, step, form, **kwargs):
    self.storage.current_step = step
    return self.render(form, **kwargs)
WizardView.get_form_step_data(form)

Этот метод получает данные из экземпляра формы form и возвращает словарь. Вы можете использовать этот метод для управления значениями перед сохранением данных в хранилище.

Стандартная реализация:

def get_form_step_data(self, form):
    return form.data
WizardView.get_form_step_files(form)

Этот метод возвращает файлы формы. Вы можете использовать этот метод для управления файлами перед их размещением в хранилище.

Стандартная реализация:

def get_form_step_files(self, form):
    return form.files
WizardView.render(form, **kwargs)

Этот метод вызывается после обработки POST или GET запроса. Вы можете внедриться в этот метод, например, для изменения типа HTTP запроса.

Стандартная реализация:

def render(self, form=None, **kwargs):
    form = form or self.get_form()
    context = self.get_context_data(form=form, **kwargs)
    return self.render_to_response(context)
WizardView.get_cleaned_data_for_step(step)

Этот метод возвращает проверенные данный для указанного шага. Перед возвращением проверенных данных, сохранённые данные проверяются с помощью формы. Если данные не проходят проверку, то возвращается None.

WizardView.get_all_cleaned_data()

Этот метод возвращает объединённый словарь с проверенными данными всех шагов. Если шаг содержит набор форм, то его ключ будет иметь префикс formset- и содержать список словарей с проверенными данными. Следует отметить, что если два или более шагов будут иметь поле с одинаковым именем, значение этого поля из более позднего шага перезапишет значение более раннего шага.

Назначение начальных данных для форм

WizardView.initial_dict

Начальные данные для экземпляров форм Form мастера могут быть предоставлены с помощью необязательного именованного аргумента initial_dict. Аргумент должен представлять собой словарь, который связывает шаги со словарями начальных данных. Словарь начальных данных будет передан конструктору формы Form соответствующего шага:

>>> from myapp.forms import ContactForm1, ContactForm2
>>> from myapp.views import ContactWizard
>>> initial = {
...     '0': {'subject': 'Hello', 'sender': 'user@example.com'},
...     '1': {'message': 'Hi there!'}
... }
>>> # This example is illustrative only and isn't meant to be run in
>>> # the shell since it requires an HttpRequest to pass to the view.
>>> wiz = ContactWizard.as_view([ContactForm1, ContactForm2], initial_dict=initial)(request)
>>> form1 = wiz.get_form('0')
>>> form2 = wiz.get_form('1')
>>> form1.initial
{'sender': 'user@example.com', 'subject': 'Hello'}
>>> form2.initial
{'message': 'Hi there!'}

Аргумент initial_dict также может принимать список словарей для определённого шага, если на этом шаге используется набор форм.

Changed in Django 1.6.

Словарь initial_dict также может быть добавлен в класс в виде атрибута initial_dict, чтобы не определять начальные данные в urls.py.

Обработка файлов

WizardView.file_storage

Для того, чтобы обработать поле FileField на любой форме мастера, вам потребуется добавить атрибут file_storage в вашу реализацию класса WizardView.

Это хранилище будет временно хранить загруженные файлы мастера. Атрибут file_storage должен быть потомком класса Storage.

Django предоставляет встроенный класс хранилища (см. классы встроенных хранилищ):

from django.conf import settings
from django.core.files.storage import FileSystemStorage

class CustomWizardView(WizardView):
    ...
    file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))

Предупреждение

Не забывайте удалять старые файлы, так как WizardView не удаляет файлы , независимо от того, удачно ли отработал мастер или нет.

Условная логика

WizardView.condition_dict

Метод as_view() принимает аргумент condition_dict. Вы можете передать словарь булевых значений или вызываемых методов. Ключ должен соответствовать имени шага (т.е. ‘0’, ‘1’).

Если значение определенного шага является функцией, она будет вызвана с экземпляром WizardView в качестве аргумента.Если эта функция вернёт true, то будет использована форма шага.

Этот пример показывает контактную форму со встроенным условием. Условие используется для отображения на форме сообщения в случае, если чекбокс на форме первого шага отмечен.

Шаги определяются в файле forms.py:

from django import forms

class ContactForm1(forms.Form):
    subject = forms.CharField(max_length=100)
    sender = forms.EmailField()
    leave_message = forms.BooleanField(required=False)

class ContactForm2(forms.Form):
    message = forms.CharField(widget=forms.Textarea)

Мы определяем наш мастер в файле views.py:

from django.shortcuts import render_to_response
from django.contrib.formtools.wizard.views import SessionWizardView

def show_message_form_condition(wizard):
    # try to get the cleaned data of step 1
    cleaned_data = wizard.get_cleaned_data_for_step('0') or {}
    # check if the field ``leave_message`` was checked.
    return cleaned_data.get('leave_message', True)

class ContactWizard(SessionWizardView):

    def done(self, form_list, **kwargs):
        return render_to_response('done.html', {
            'form_data': [form.cleaned_data for form in form_list],
        })

Теперь надо добавить ContactWizard в файл urls.py:

from django.conf.urls import patterns

from myapp.forms import ContactForm1, ContactForm2
from myapp.views import ContactWizard, show_message_form_condition

contact_forms = [ContactForm1, ContactForm2]

urlpatterns = patterns('',
    (r'^contact/$', ContactWizard.as_view(contact_forms,
        condition_dict={'1': show_message_form_condition}
    )),
)

Как вы можете видеть, мы определили show_message_form_condition сразу за нашей реализацией класса WizardView и добавили аргумент condition_dict в метод as_view(). Ключ ссылается на второй шаг мастера, т.к индекс шагов начинается с нуля.

Как работать с ModelForm и ModelFormSet

WizardView.instance_dict

Мастер может использовать ModelForms и ModelFormSets. В дополнение к атрибуту initial_dict, метод as_view() принимает аргумент instance_dict, который должен содержать экземпляры ModelForm и ModelFormSet. Аналогично атрибуту initial_dict, ключи этих словарей должны соответствовать номеру шага в списке форм.

Использование NamedUrlWizardView

class NamedUrlWizardView
class NamedUrlSessionWizardView
class NamedUrlCookieWizardView

Существует потомок класса WizardView, который добавляет в мастер поддержку именованных URL. Используя его, вы можете иметь уникальный URL для каждого шага мастера. Вы также можете использовать классы NamedUrlSessionWizardView или NamedUrlCookieWizardView, которые заранее выбирают бэкенд для хранения информации (сессии на стороне сервера и куки браузера, соответственно).

Для использования именованных URL, потребуется внести изменения в файл urls.py.

Ниже представлен пример мастера контактов с двумя шагами, первый шаг называется «contactdata», а второй — «leavemessage».

Дополнительно придётся передать два аргумента в метод as_view():

  • url_name – название URL (как указано в urls.py).

  • done_step_name – имя в URL для пройденного шага.

Пример измененного файла urls.py:

from django.conf.urls import url, patterns

from myapp.forms import ContactForm1, ContactForm2
from myapp.views import ContactWizard

named_contact_forms = (
    ('contactdata', ContactForm1),
    ('leavemessage', ContactForm2),
)

contact_wizard = ContactWizard.as_view(named_contact_forms,
    url_name='contact_step', done_step_name='finished')

urlpatterns = patterns('',
    url(r'^contact/(?P<step>.+)/$', contact_wizard, name='contact_step'),
    url(r'^contact/$', contact_wizard, name='contact'),
)

Дополнительные методы NamedUrlWizardView

NamedUrlWizardView.get_step_url(step)

Этот метод возвращает URL для указанного шага.

Стандартная реализация:

def get_step_url(self, step):
    return reverse(self.url_name, kwargs={'step': step})