terça-feira, 18 de setembro de 2012

Validadores Personalizados (Custom Validators) no Web2py

No post sobre Validadores no Web2py mostrei como utilizar alguns dos validadores que o Web2py nos oferece. Estes são validadores de uso geral e atendem a maioria das necessidades das aplicações web.
Caso nossa aplicação possua uma necessidade específica de validação, que não existe nos validadores nativos do Web2py, podemos criar nossos próprios validadores personalizados (custom validators) de forma simples.
Como sempre, o Livro do Web2py nos mostra o caminho.

Um Problema

Vamos imaginar uma situação onde haja a necessidade de uma validação de dados específica na aplicação. 
Por exemplo, imagine que em uma determinada página haja um formulário com uma série de campos de texto. O preenchimento dos campos de texto não é obrigatório, eles podem ser deixados em branco. Mas caso sejam preenchidos, devem seguir a seguinte regra lógica: 
  • o segundo campo de texto só pode ser preenchido se o primeiro campo também for preenchido; 
  • o terceiro campo de texto só pode ser preenchido se o segundo e o primeiro também forem, e assim por diante.
Caso estas regras não sejam respeitadas, o validador não deve permitir que os dados do formulário sejam submetidos.
Não existe um validador padrão que atenda esta necessidade tão específica da nossa aplicação. Mas temos a opção de criar um.

Um Detalhe

Note que não precisamos necessariamente criar um validador personalizado para resolver o problema acima. O Web2py oferece outras soluções, como por exemplo o argumento "onvalidation" que permite que uma função seja chamada no momento que o formulário é submetido (form.process()) podendo retornar um erro no formulário se os dados não atenderem às necessidades da aplicação.
Para mais informações sobre o argumento onvalidation acesse esta página.
No entanto, criaremos um validador personalizado por ser o objetivo do post e por esta solução permitir que o código seja facilmente reutilizado no futuro, se houver necessidade.

Uma Solução

Os validadores do Web2py são classes que precisam basicamente de 3 métodos, se você já está familiarizado com orientação a objetos no Python será ainda mais fácil: 
  • __init__ - chamado dentro do formulário na definição do validador.
  • __call__ - chamado no processamento do formulário. Este é o método que faz a validação propriamente dita. Caso os dados estejam corretos ele retorna apenas o valor. Caso haja erro de validação, ele retorna o valor e a mensagem de erro definida.
  • formatter - método especial responsável por retornar o valor resultante (útil quando há necessidade de conversão de tipos de dados). No nosso caso ele simplesmente retornará o valor que recebe sem modificações.
Oberve o modelo de validador do Web2py:
class sample_validator:
    def __init__(self, *a, error_message='error'):
        self.a = a
        self.e = error_message
    def __call__(self, value):
        if validate(value):
            return (parsed(value), None)
        return (value, self.e)
    def formatter(self, value):
        return format(value)

Para o nosso exemplo, o método __init__ receberá uma lista contendo as variáveis dos campos de texto do formulário. O método __call__ verificará se esta lista é válida de acordo com a regra estabelecida.
O novo validador será definido no modelo para ficar disponível no escopo da aplicação.
Neste caso, vou criar um novo arquivo chamado validators.py no modelo. O conteúdo deste arquivo é o seguinte:
# coding: utf8
class DEPENDE_DE(object):
    def __init__(self, lista, error_message='Erro de dependência'):
        self.l = lista
        self.e = error_message
    def __call__(self, valor):
        if valor != "":
            ctr = self.l[0]        
            for i in range(1, len(self.l)):
                if ctr == "" and self.l[i] != "":
                    return (valor, self.e)                
                else:
                    ctr = self.l[i]
        return (valor, None)
    def formatter(self, valor):
        return valor

A lógica é bem simples.
O __init__ receberá a lista com os dados preenchidos nos campos de texto. Esta lista representa os próprios campos de texto, ou seja, o primeiro item da lista recebe o conteúdo do primeiro campo, o segundo item da lista recebe o conteúdo do segundo campo e assim por diante.
O __call__ será chamado no momento da validação.
Caso o valor que ele receba seja uma string vazia, ele retornará o valor sem erros automaticamente, pois validaremos apenas os campos que foram preenchidos.
Se o valor recebido não for vazio, o método iniciará a validação. Inicialmente ele define o primeiro item da lista como o controle (ctr) e faz a iteração sobre o restante da lista. No final do laço for, o controle é atualizado para o próximo item da lista. Se em algum momento do laço for houver uma combinação de string não-vazia após uma string vazia, a validação não ocorre e o método retorna o valor recebido e a mensagem de erro do validador.
Se a lista for iterada sem encontrar a combinação definida, a validação é confirmada e o valor é retornado sem erro.

Agora precisamos implementar este validador em um formulário para ver se ele funciona da forma como queremos.
No controlador default.py eu criei a seguinte função:
def lista():
    form = SQLFORM.factory(
    Field('Item1'),
    Field('Item2', requires=DEPENDE_DE([request.vars.Item1, request.vars.Item2])),
    Field('Item3', requires=DEPENDE_DE([request.vars.Item1, request.vars.Item2, request.vars.Item3])))
    if form.process().accepted:
        response.flash = 'Formulário ok!'
    elif form.errors:
        response.flash = 'Erros no formulário!'
    return dict(form=form)

Note que eu defini como argumento do validador uma lista com as variáveis do campo atual e dos que o precedem. A exceção é o primeiro campo, que, pela lógica, não precisa de validador. Desta forma o validador funciona independentemente do número de campos que eu utilizar.
A view default/lista.html precisa apenas de:
{{extend 'layout.html'}}

{{=form}}
Se tudo correr bem, o resultado deve ser um formulário semelhante a este:
Realizando alguns testes, é possível verificar que as regras que definimos no nosso problema são respeitadas pelo validador.
Nenhum campo é obrigatório, se eu clicar no botão Submit sem preencher nada o formulário é aceito.
Obviamente, se eu preencher todos os campos, o formulário também é aceito sem problemas.
Agora vejamos o que acontece se eu preencher apenas o Item2 e o Item3:
E se eu preencher apenas o Item2:
O Item3 não retorna erro de validação porque segundo a nossa regra ele não precisa ser preenchido e nosso validador ignora campos não preenchidos.
Veja outro exemplo:
Neste caso, embora o Item1 tenha sido preenchido, o Item3 só pode receber um valor se o Item2 também receber.
Caso o Item1 e o Item2 sejam preenchidos e o Item3 esteja em branco, o validador não retornará erro algum.
Note que este validador também possui o argumento opcional "error_message" que permite personalizar a mensagem de erro:
Para finalizar, observe que utilizei o SQLFORM.factory() neste exemplo apenas para agilizar o processo de criação do formulário e obter um layout mais adequado sem muito esforço.
O resultado final será o mesmo caso você utilize o FORM().

Nenhum comentário:

Postar um comentário