quinta-feira, 13 de setembro de 2012

Formulários com SQLFORM no Web2py

Sem dúvida um dos elementos chave de qualquer aplicação web é o formulário. Ele permite o envio de dados entre o usuário e o app.
Este elemento possui um mecanismo simples mas que pode se desdobrar para atender quase qualquer necessidade do programador.
No caso do Web2py, o Livro tem uma seção específica sobre o assunto. Nele são apresentadas as diversas maneiras de se criar e configurar formulários utilizando as ferramentas que o framework oferece.
E o meu favorito é o SQLFORM. O SQLFORM é sensacional. Com ele você pode criar um formulário mais rápido que porta na cara de Testemunha de Jeová.
A seguir vou mostrar alguns exemplos do que o SQLFORM é capaz de fazer.
Vamos supor que nossa aplicação terá uma opção para que os usuários submetam seu nome e email de contato para receber atualizações do projeto.
Considere a seguinte tabela no modelo:  
db.define_table('contatos',
Field('nome', 'string'),
Field('email', 'string'),
Field('dtcadastro', 'date'),
Field('status', 'string', requires=IS_IN_SET(['Ativo', 'Inativo'])))

A tabela contatos deve ser preenchida com o nome do usuário, o email, a data do cadastro e o status Ativo ou Inativo.
Utilizando o SQLFORM é possível criar um formulário para esta tabela com os seguintes comandos.
No controlador default.py:
def contatos():
    form = SQLFORM(db.contatos)
    if form.process().accepted:
        response.flash = 'Contato cadastrado!'
    return dict(form=form)

E na view default/contatos.html:  
{{extend 'layout.html'}}
{{=form}}

O resultado deve ficar semelhante a este:

Este simples formulário já é totalmente funcional. Preenchendo os dados corretamente, o contato será inserido na tabela contatos que criamos no banco de dados.
Porém, este formulário não está exatamente aceitável para nossa aplicação.
Em primeiro lugar, como a aplicação está em português, não faz sentido que o botão que envie os dados do formulário apareça em inglês, certo?
Isso é fácil de resolver e uma das formas de fazer isso é definir manualmente o nome que o botão submit do formulário deve exibir.
Para isso, vamos alterar a seguinte linha do controlador:
form = SQLFORM(db.contatos, submit_button='Salvar')

E o resultado disso é: 
Outro detalhe que precisa ser corrigido é que o formulário atribui como label dos campos de texto o mesmo nome dos campos definidos na tabela do banco de dados automaticamente. Mas não podemos deixar o campo se chamar Dtcadastro. Então vamos alterar isto também de forma bem simples.
Na realidade, podemos personalizar o nome de cada um dos campos do formulário utilizando um dicionário onde a chave (key) é o nome do campo na tabela e o valor (value) é o nome que queremos exibir.
Neste caso, o que queremos é isso:

form = SQLFORM(db.contatos, submit_button='Salvar',
               labels={'dtcadastro':'Data de Cadastro'})

Veja como fica:
Pensando bem, este formulário ainda não está bom.
Na realidade, temos informações desnecessárias sendo exibidas.
Para o funcionamento correto da nossa aplicação, precisamos saber o nome e o email de contato do usuário, bem como a data em que o cadastro foi feito e se este contato tem status ativo ou inativo.
Porém nem todas essas informações precisam ser fornecidas pelo usuário.
Obviamente a data de cadastro é a data em que este formulário é submetido, ou seja, sempre a data atual. Não há necessidade de questionar isso para o usuário. De fato, permitir que o usuário defina a data de cadastro pode se tornar um problema, uma vez que não há garantia de que o usuário irá preencher a data correta.
Seguindo esta linha de raciocínio, no momento em que o cadastro do contato é realizado, o status dele será sempre ativo. Não existe motivo para cadastrar um contato inativo. Esta opção só será útil após o cadastro ser realizado. Portanto, também não é preciso solicitar que o usuário preencha esta informação.
O SQLFORM cria o formulário automaticamente com todos os campos definidos na tabela.
Em uma aplicação mais complexa, seria interessante definir na tabela apenas os campos que serão necessários no formulário e definir uma segunda tabela, com relacionamento de um para um, com os dados adicionais que são definidos no back-end.
No nosso exemplo, podemos definir que o SQLFORM crie o formulário apenas com alguns campos da tabela.
Veja o exemplo:
form = SQLFORM(db.contatos, submit_button='Salvar',
               fields=['nome', 'email'])

Observe que eu removi o argumento que alterava a label do campo Dtcadastro uma vez que este campo não fará mais parte do formulário.
Podemos definir quais campos da tabela farão parte do formulário utilizando o argumento fields que recebe uma lista de strings conforme exemplo acima.
O formulário ficou assim:
Bom. Agora que o formulário recebe apenas os dados relevantes do usuário, ainda precisamos definir como vamos inserir as informações que ficaram de fora.
Existem várias formas de fazer isso.
Neste exemplo, vamos criar uma função no modelo que será chamada imediatamente após os dados do formulário serem submetidos. Esta função será responsável por completar o cadastro do contato.
Desta forma, podemos reutilizar a função em outras situações.
Veja como ficou a função que defini no modelo:
from datetime import date

def completarCadastro(idContato):
    try:
        hoje = date.today()
        db(db.contatos.id == idContato).update(dtcadastro=hoje, status='Ativo')
        return True
    except:
        return False

Ou seja, a função recebe o id do contato e realiza um update neste registro na tabela do banco de dados para completar o cadastro com as informações padrão.
O controlador ficou assim:
def contatos():
    form = SQLFORM(db.contatos, submit_button='Salvar', 
               fields=['nome', 'email'])
    if form.process().accepted and completarCadastro(form.vars.id):
        response.flash = 'Contato cadastrado!'
    elif form.errors:
        response.flash = 'Erro ao cadastrar! Verifique os dados.'
    return dict(form=form)

A aplicação considera o cadastro bem-sucedido se o formulário for submetido sem erros E se a função que completa o cadastro retornar True.
Note que para este exemplo não incluí nenhuma validação nos campos.
Este assunto será abordado em um próximo post.

Tem Mais

Além de todas essas possibilidades que o SQLFORM do Web2py oferece, às vezes ainda precisamos ter um pouco mais de controle sobre a maneira como o formulário é exibido na view. 
Se você analisar o código fonte do formulário gerado pelo SQLFORM, irá notar que ele gera ids e classes para os elementos do formulário automaticamente, ou você mesmo pode definir utilizando o argumento _class, por exemplo.
Mas se tudo isso ainda não for suficiente para obter o resultado desejado na view, ainda é possível construir o formulário todo "na mão".
{{extend 'layout.html'}}

{{=form.custom.begin}}
<label for="Nome"><span class="formContatoNome">Nome</span></label>
    {{=form.custom.widget.nome}}
<br />

<label for="Email"><span class="formContatoEmail">Email</span></label>
    {{=form.custom.widget.email}}
<br />

<input id="formContatoBt" name="ok" type="submit" value="Salvar" />
  <br />

{{=form.custom.end}}

Utilizando o form.custom é possível definir manualmente cada aspecto do formulário sem precisar alterar nada do que foi definido no controle ou no modelo. Basta posicionar os campos de texto nos locais desejados da forma como mostrada no exemplo.
Por enquanto é isso.
Mais informações e detalhes sobre o maravilhoso SQLFORM você encontra no Livro.

5 comentários:

  1. Bem legal!

    Um detalhe é que você pode substituir a função completarCadastro por:

    db.contatos.dtcadastro.default = date.today()
    db.contatos.status.default = "Ativo"
    form = SQLFORM(db.contatos)
    ..
    ..

    Assim os dois campos em questão serão automaticamente inseridos no submit do form.

    Mas seu exemplo foi bem legal por mostrar que é possível manipular os dados submetidos.

    Outro jeito de fazer isso seria com uma função de callback.

    def completarcadastro(form):
    #faça qualquer coisa com form.vars

    form = SQLFORM(db.contatos)
    if form.process(onvalidate=completarcadastro).accepted:
    # sucesso

    ou também pode usar

    form = SQLFORM(db.contatos)
    if form.process(onaccept=completarcadastro).accepter:
    #sucesso

    a diferença é que o onvalidation é chamado antes do insert/update ocorrer e permite manipular o form.errors para exibir mensagens de erro adicionais no formulário.

    ResponderExcluir
    Respostas
    1. Mestre!
      Eu cito o onvalidation no post sobre validators, mas o primeiro exemplo nem me passou pela cabeça.
      Talvez os últimos 6 meses trabalhando com Java tenham me afetado, mas eu ganhei gosto por manter funções auxiliares no modelo, sobretudo quando manipulam banco de dados, e deixar os controllers o mais limpo possível.
      O exemplo simplista do post não reflete essa necessidade.
      Muita obrigado pelas dicas e pela ilustre presença hehe.

      Excluir
  2. Ola, bom dia! sou iniciante nesse mundo web de programacao, gostaria de saber, estou usando o SQLFORM igual a vc, como controlo o tamanho do campo, pois meu campo NOME aparece bem pequeno na view, ja o campo DATA aparece grande d+, queria poder definir o tamanho deles... outra coisa como mostro um campo na frente do outro, em vez de um a baixo do outro... pretendo fazer um curso de HTML5 e CSS3... se puder me ajudar agradeço, suas dicas acima ja foram de grande valia.
    Att:Carlinhos(alto paraiso de goias, email: carlynhos77@hotmail.com)

    ResponderExcluir
    Respostas
    1. Bom dia Carlinhos.
      O SQLFORM é uma forma rápida de fazer formulários. Ele é rápido porque ele faz quase tudo automaticamente. Mas não é a única forma de se fazer formulários no web2py. Na realidade o FORM ainda é a forma mais comum de se criar um formulário. Eu o aconselho a dar uma pesquisada sobre ele para ver se não atende melhor as suas necessidades.
      As questões que você levantou sobre tamanho e posição de campos podem ser solucionadas utilizando o custom.form (eu falo dele no final deste artigo). Assim você cria, no HTML mesmo, os campos da forma que quiser, e só posiciona os widgets nos locais desejados. Peque o exemplo que eu coloquei ali e teste no seu projeto. Se você ainda não tem conhecimento em HTML, sugiro que faça o curso pois será de grande importância.
      No inicio do artigo tem um link para a seção de formulários do livro do web2py, se você entende inglês é uma boa fonte para mais informações. Mas também tem uma ótima versão do livro em português. Não tenho o link aqui mas é bem fácil de achar no Google.
      Obrigado pelo comentário e boa sorte!

      Excluir
    2. Jonathan e Carlinhos, mesmo usando o SQLFORM é possível personalizar o tamanho dos campos.

      Quando o Web2py renderiza o form gerado pelo SQLFORM, cada campo é mostrado com uma tag HTML com um id. Assim, é possível configurar não só o tamanho, mas qualquer propriedade dele via CSS.

      Para detalhes, dê uma olhada no código fonte da página HTML gerada.

      Observação: sempre que possível, tente usar SQLFORM ou SQLFORM.factory() para interações com o banco de dados. Essas duas opções permitem usar os validadores definidos no seu model. O FORM simples não permite isso.

      Excluir