terça-feira, 3 de setembro de 2013

Web2py, jQuery e AJAX: criando um componente de enquetes

O objetivo deste artigo é criar um componente para enquetes simples no web2py. Assim veremos detalhes de como funcionam os componentes no web2py, além de utilizar efeitos do jQuery na tela e utilizar o AJAX para se comunicar com o servidor.
No final do artigo, você encontra um link para uma versão de demonstração do componente criado aqui.

Componentes


Os componentes são a chave para a reusabilidade do código no desenvolvimento para a web. Basicamente, um componente é um elemento independente que realiza uma tarefa específica em uma página web. Ele deve ser capaz de ser facilmente "embebido" em qualquer página da aplicação. Algumas tarefas comuns que costumam ser realizadas com componentes são registros de comentários, breadcrums, avaliação de conteúdo (ratings), formulários de pesquisa, etc. Para satisfazer a necessidade de ser um elemento independente do restante da página, é fundamental que a comunicação do componente com o servidor web seja via AJAX. Para isso, o web2py possui a função LOAD.

Cadastro de Enquetes


Para demonstrar como funcionam os componentes no web2py, vamos criar um componente para enquetes simples que nos permita incluir enquetes em qualquer página de forma rápida e fácil.
Vamos começar pelo começo: o modelo.
São duas tabelas: uma com as enquetes e outra com os itens das enquetes e o registro dos votos.
db.define_table('enquetes',
Field('titulo','string',required=True, 
      requires=IS_NOT_EMPTY(error_message="Este campo é obrigatório")),
Field('descricao', 'text',label="Descrição"),
Field.Method('total_votos', lambda r: sum([i.votos for i in 
             db(db.itens.enquete==r.enquetes.id).select(db.itens.votos)]))
)

db.define_table('itens',
Field('enquete','reference enquetes'),
Field('nome','string',required=True, 
      requires=IS_NOT_EMPTY(error_message="Este campo é obrigatório")),
Field('votos','integer', default=0)
)

db.itens.votos.writable = db.itens.votos.readable = False
db.itens.enquete.writable = db.itens.enquete.readable = False

Observe que a tabela de enquetes contém um campo virtual para calcular o total de votos. Não pretendo entrar em detalhes sobre campos virtuais neste artigo. Talvez em um artigo futuro. Por ora, consulte o Livro para saber mais sobre Field.Virtual e Field.Method.
O próximo passo é criar a tela de cadastro das enquetes. Para isso, vamos criar o controller enquetes.py e, dentro dele, a função criar:
def criar():
    form = SQLFORM.factory(Field('titulo',requires=IS_NOT_EMPTY()),
                           Field('descricao', 'text', label='Descrição'),
                           *[Field('op%i'%n,label='Opção %i'%n) for n in range(1,11)],
                           submit_button="Criar Enquete")
    if form.process().accepted:
        enquete = db.enquetes.insert(titulo=form.vars.titulo,
                                     descricao=form.vars.descricao)
        for item in form.vars:
            if item not in ('titulo','descricao') and form.vars[item]:
                db.itens.insert(enquete=enquete,nome=form.vars[item])
    return dict(form=form)

Para evitar a fadiga não perder tempo, vamos utilizar o SQLFORM.factory para criar o formulário de cadastro das enquetes. Vamos permitir que o usuário defina quantas opções de votação cada enquete terá, mas vamos limitar a no máximo 10 itens. Como não sabemos quantos itens o usuário irá cadastrar, vamos criar os 10 campos e com a ajuda do jQuery, permitir que, no momento do cadastro, os campos possam ser adicionados à medida que os dados são inseridos. Isso resulta em um efeito bacana e, ao mesmo tempo, evita chamadas desnecessários ao servidor para criar mais campos.
Agora precisamos configurar a view. A tela enquetes/criar.html é bem simples:
{{extend 'layout.html'}}
{{=form}}

Apenas exibimos o formulário de cadastro e carregamos o código jQuery que irá ocultar todos os campos de itens da enquete. Depois exibimos o primeiro campo e adicionamos uma função para exibir o próximo campo assim que o anterior for preenchido, apenas para dar um efeito legal. Se quiser, pode apenas carregar o formulário todo sem jQuery.
Note que o seletor jQuery busca por "no_table_op" porque este é o padrão de ID's que o SQLFORM.factory() gera ("no_table_"+nome do campo - neste caso, os nomes dos campos que defini no controller são "op1","op2",etc.). Em caso de dúvidas, apenas consulte o html gerado para o formulário na saída.
Note também que não precisamos adicionar o jQuery na tela neste caso porque o layout.html padrão do web2py já inclui o jQuery.
No fim, a tela de cadastro de enquetes deve ficar mais ou menos assim:

Votação


Agora é a hora do AJAX entrar em ação. É imprescindível que componentes se comuniquem com o servidor de forma assíncrona para que não interfiram no processamento do restante da aplicação. Para variar, o web2py facilita a nossa vida. O jeito mais fácil de chamar um componente é a função LOAD.
No web2py o funcionamento de um componente em nada difere de qualquer outro elemento que pode ser desenvolvido em uma aplicação. Precisamos criar uma função no controller, que pode ou não acessar recursos do model, e associar uma view para exibir os dados.
Então vamos começar definindo a função que irá exibir a enquete e permitir a votação. Faremos isso também no arquivo enquetes.py:
def votar():
    if request.vars.id:
        # Se receber um id, seleciona a enquete especificada
        enquete = db(db.enquetes.id==request.vars.id).select().first()
    else:
        # Senão consulta todas as enquetes e seleciona a última
        enquete = db().select(db.enquetes.ALL).last()
    # Cria um fieldset de radiobuttons com todos os itens da enquete. O _value será o id do item
    # para que possamos identificar qual item foi selecionado na votação
    campos = [FIELDSET(INPUT(_type='radio',_name=item.nome, _value=item.id),"   "+item.nome) 
                                for item in enquete.itens.select()]
    # Adiciona um botão Votar na lista de campos
    campos.append(INPUT(_type='submit',_value='Votar'))
    # Cria o formulario a partir da lista de campos definida
    form = FORM(*campos)
    if form.process().accepted:
        # Se o formulário for validado, itera sobre as variáveis retornadas
        for item in form.vars:
            # Apenas o item que foi selecionado contem um valor (o id dele)
            if form.vars[item]:
                # E com o id podemos consultar o item no banco
                item = db(db.itens.id==form.vars[item]).select().first()
                # Adicionar mais um voto
                item.votos = item.votos + 1
                # Atualizar o registro no banco
                item.update_record()
                # E retornar a enquete atualizada
                enquete = db(db.enquetes.id==enquete.id).select().first()
                return dict(enquete = enquete, 
                            itens = enquete.itens.select(orderby=~db.itens.votos))
    return dict(enquete=enquete,form=form)
No código acima, vamos selecionar a enquete de acordo com o id recebido por parâmetro na URL. Se não recebermos parâmetro de id, simplesmente selecionamos a última enquete criada. Depois, criamos o Fieldset dos itens da enquete para ser adicionado no formulário (neste caso, utilizaremos o FORM, porque o html gerado deve ser o mais limpo possível).
Após a votação, a opção votada retornará o id nas variáveis do formulário (form.vars) conforme definimos no campo _value do Fieldset. Assim, conseguimos selecionar o item em questão, adicionar um voto a mais e fazer o update do registro.
Por fim, retornaremos a enquete atualizada para que o componente possa exibir o resultado da votação.
Agora criaremos a parte visual do componente. Uma diferença aqui é que ao invés de ser um arquivo de extensão .html será um arquivo de extensão .load. Outra diferença é que não iremos estender do layout padrão do web2py, ou de qualquer outro. Devemos evitar associar estilos no componente para que não hajam conflitos com a página exterior. Portanto, o arquivo enquetes/votar.load fica assim:
{{=H4(enquete.titulo)}}
{{=H5(enquete.descricao)}}
{{if 'form' in globals():}}{{=form}}
{{else:}}
{{for item in itens:}}
{{=DIV(item.nome)}}
{{=DIV(str(item.votos), _style="color:"+("white" if item.votos else "black")+";background-color:blue;width:"+
       str((float(item.votos)/float(enquete.total_votos()))*100)+"%;")}}
{{pass}}
{{pass}}

Se o componente receber o formulário que criamos, ele apenas o exibe. Caso contrário, o formulário já foi submetido e ele deve apenas exibir os resultados da enquete.

Chamando o Componente


Agora só precisamos adicionar o componente na nossa aplicação. Para este exemplo, vou utilizar a página da aplicação welcome padrão do web2py.
Tudo o que precisamos fazer é utilizar a função LOAD, como no exemplo:
{{=LOAD('enquetes','votar.load',ajax=True)}}
O resultado ficou assim:

Como definimos na função votar do controller de enquetes, quando não passamos um parâmetro de id, a última enquete cadastrada é exibida. Agora, se quisermos exibir uma enquete específica, basta alterar o comando de chamada:
{{=LOAD('enquetes','votar.load?id=7',ajax=True)}}
Assim, a enquete com o id 7 será exibida.
Para testar a aplicação, acesse este link, onde o mesmo componente foi utilizado.
Por enquanto, é isso.

Um comentário: