Form wizard

O Django vem acompanhado uma aplicação opicional “form wizard” que divide formulários em várias páginas Web. Ele mantém o estado num hash HTML de campos <input type="hidden">, e os dados não são processados no lado do servidor até que a última parte do formulário seja submetida.

Você pode querer usar isto com formulários muito compridos que seriam desagradáveis de se mostrar numa única página. A primeira página pode pedir ao usuário informações centrais, a segunda coisas menos importantes, e assim por diante.

o termo “wizard” (em português “assistente”) neste contexto, é explicado na Wikipedia.

Como ele funciona

Aqui temos o fluxo de funcionamento básico de como um usuário poderia usar um wizard:

  1. O usuário visita a primeira página do wizard, preenche o formulário e o submete.
  2. O servidor valida os dados. Se eles forem inválidos, o formulário é mostrado novamente, com mensagens de erro. Se for válido, o servidor calcula um hash seguro dos dados e apresenta ao usuário o próximo formulário, poupando os dados validados e o hash em campos <input type="hidden">.
  3. Repete o passo 1 e 2, para todos os formulários subsequentes do wizard.
  4. Um vez que o usuário tenha submetido todos os formulários e todos os dados estejam validados, o wizard processa os dados – salvando-os no banco de dados, enviando um e-mail, ou seja lá o que for que a aplicação precise fazer.

Uso

Esta aplicação manipula tantos mecanismos para você quanto possível. Geralmente, você só tem que fazer estas coisas:

  1. Definir um número de classes Form – uma por página do wizard.
  2. Criar uma classe FormWizard que especifica o que fazer uma vez que todos os seus formulários foram submetidos e validados. Isso também permite você sobrescrever alguns comportamentos do wizard.
  3. Criar alguns templates que renderizam formulários. Você pode definir um único template genérico para manipular todos os formulários, ou definir um template específico para cada formulário.
  4. Apontar seu URLconf para sua classe FormWizard.

Definindo classes Form

o primeiro passo para criar um “form wizard” é criar as classes Form. Estes devel ser classses django.forms.Form padrão conforme explicado na documentação do forms. Estas classes podem estar em qualquer lugar de sua base de código, mas por convenção são postas num arquivo chamado forms.py dentro da sua aplicação.

Por exemplo, vamos escrever um wizard “contact form”, onde a primeira página coleta o endereço de email do remetente e o assunto, e a segunda página coleta a mensagem em si. Aqui tem um exemplo de como o forms.py pode ficar:

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)

Limitação importante: Pelo wizard usar campos HTML hidden para armazenar dados entre as páginas, você não pode incluir um FileField em qualquer outra página, a não ser na última.

Criando uma classe FormWizard

O próximo passo é criar uma classe FormWizard, que deveria ser uma subclasse do django.contrib.formtools.wizard.FormWizard.

Assim como suas classes Form, esta classe FormWizard pode estar em qualquer lugar da sua base de código, mas por convenção é colocado no forms.py.

O único requerimento desta subclasse é que ela implementa um método done(), que especifica o que deve acontecer quando todos os formulários foram submetidos e validados. A este método é passado dois argumentos:

  • request -- um objeto HttpRequest
  • form_list -- uma lista de classes django.forms Form

Neste exemplo simplista, ao invés de executar qualquer operação de banco de dados, o método simplesmente renderiza um template com os dados validados:

from django.shortcuts import render_to_response
from django.contrib.formtools.wizard import FormWizard

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

Note que este método será chamado via POST, então ele realmente deveria ser um bom cidadão Web e redirecionar depois de processar os dados. Aqui tem outro exemplo:

from django.http import HttpResponseRedirect
from django.contrib.formtools.wizard import FormWizard

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

Veja a seção Métodos avançados do FormWizard abaixo para aprender mais sobre os hooks do FormWizard.

Criando templates para formulários

Agora, precisamos criar um template que renderize os forumlários do wizard. Por padrão, todo formulário usa um template chamado forms/wizard.html. (Você pode mudar este nome sobrescrevendo o método get_template(), que está documentado abaixo. Este hook também permite você usar um template diferente para cada formulário.)

Este template aguarda o seguinte contexto:

  • step_field -- O nome do campo hidden contento o passo.
  • step0 -- O passo atual (partindo de zero).
  • step -- O passo atual (partindo de um).
  • step_count -- O total de passos.
  • form -- A instância do Form para o passo atual (vazio ou com erros).
  • previous_fields -- Uma string representando todos os campos de dados anteriores, mais os hashes de formulários completados, todos em campos hidden de formulário. Note que você precisará executar isto através do template filter safe(), para previnir o auto escape, por ele é um HTML puro.

Ele também será transmitido a quaisquer objetos no extra_context, que é um dicionário que você pode especificar contendo valores extra a serem adicionados ao contexto. Você pode especificá-lo de duas formas:

  • Sete o atributo extra_context na sua subclasse FormWizard para um dicionário.
  • Passe extra_context como parâmetros extra no URLconf.

Aqui tem um exemplo de template completo:

{% extends "base.html" %}

{% block content %}
<p>Step {{ step }} of {{ step_count }}</p>
<form action="." method="post">
<table>
{{ form }}
</table>
<input type="hidden" name="{{ step_field }}" value="{{ step0 }}" />
{{ previous_fields|safe }}
<input type="submit">
</form>
{% endblock %}

Note que previous_fields, step_field e step0 são todos obrigatórios para o wizard funcionar perfeitamente.

Ligando o wizard num URLconf

Finalmente, dê ao seu novo objeto FormWizard uma URL no urls.py. O wizard recebe uma lista de seus objetos form como argumentos:

from django.conf.urls.defaults import *
from mysite.testapp.forms import ContactForm1, ContactForm2, ContactWizard

urlpatterns = patterns('',
    (r'^contact/$', ContactWizard([ContactForm1, ContactForm2])),
)

Métodos avançados do FormWizard

class FormWizard

Além do método done(), o FormWizard oferece alguns métodos avançados que permitem você customizar o funcionamento do seu wizard.

Alguns destes métodos recebem um argumento step, que é um contador, partindo de zero, representando o passo corrente do wizard. (E.g., o primeiro form é 0 e o segundo form é 1.)

FormWizard.prefix_for_step()

Dado um passo, retorna um prefixo de Form para uso. Por padrão, este simplesmente usa o passo em si. Para mais, veja a documentação do prefixo de formulário. Implementação padrão:

def prefix_for_step(self, step):
    return str(step)
FormWizard.render_hash_failure()

Renderiza um template se a verificação do hash falhar. É raro você precisar sobrescrever este método.

Implementação padrão:

def render_hash_failure(self, request, step):
    return self.render(self.get_form(step), request, step,
        context={'wizard_error': 'Desculpe-nos, mas seu formulário expirou. Por favor continue preenchendo o formulário desta página.'})
FormWizard.security_hash()

Calcula o hash de segurança para um dado objeto request e instância de Form.

Por padrão, ele usa um hash MD5 dos dados do formulário e sua configuração SECRET_KEY. É raro alguém precisar sobrescrever isso.

Exemplo:

def security_hash(self, request, form):
    return my_hash_function(request, form)
FormWizard.parse_params()

Um hook para salvar o estado do objeto request e args / kwargs que foram capiturados da URL pelo seu URLconf.

Por padrão, isso não faz nada.

Exemplo:

def parse_params(self, request, *args, **kwargs):
    self.my_state = args[0]
FormWizard.get_template()

Retorna o nome do template que deveria ser usado para um certo passo.

Por padrão, ele retorna 'forms/wizard.html', indiferente do passo.

Exemplo:

def get_template(self, step):
    return 'myapp/wizard_%s.html' % step

Se o get_template() retorna uma lista de strings, então o wizard usará a função do sistema de template select_template(), explanada na documentação de template. Isto significa que o sistema usará o primeiro template que existir no sistema de arquivo. Por exemplo:

def get_template(self, step):
    return ['myapp/wizard_%s.html' % step, 'myapp/wizard.html']
FormWizard.render_template()

Renderiza o template para o dado passo, retornando um objeto HttpResponse.

Sobrescreva este método se você deseja adicionar um contexto customizado, retornar um tipo MIME diferente, etc. Se você somente precisa sobrescrever o nome do template, use o método get_template().

O template será renderizado com o contexto documentado na seção acima "criando templates para formulários".

FormWizard.process_step()

Um hook para modificar o estado interno do wizard, dado uma totalidade de objetos Form validados. O Form está garantido a ter dados limpos e válidos.

Este método não deve modificar qualquer dado. Ao invés disso, ele pode querer setar um self.extra_context ou dinamicamente alterar o self.form_list, baseado nos formulários submetidos anteriormente.

Note que este método é chamado toda vez que uma página é renderizada para todos os passos submetidos.

A assinatura da função:

def process_step(self, request, form, step):
    # ...

Providing initial data for the forms

FormWizard.initial

Initial data for a wizard's Form objects can be provided using the optional initial keyword argument. This argument should be a dictionary mapping a step to a dictionary containing the initial data for that step. The dictionary of initial data will be passed along to the constructor of the step's Form:

>>> from testapp.forms import ContactForm1, ContactForm2, ContactWizard
>>> initial = {
...     0: {'subject': 'Hello', 'sender': 'user@example.com'},
...     1: {'message': 'Hi there!'}
... }
>>> wiz = ContactWizard([ContactForm1, ContactForm2], initial=initial)
>>> form1 = wiz.get_form(0)
>>> form2 = wiz.get_form(1)
>>> form1.initial
{'sender': 'user@example.com', 'subject': 'Hello'}
>>> form2.initial
{'message': 'Hi there!'}