Utilizando zope.formlib
Utilizando zope.formlib
1. Por que aprender um novo framework de formulários?
Você deve estar se perguntando sobre a necessidade de aprender um novo framework, ainda mais se já sabe utilizar o CMFFormController. Isto é, para quê utilizar novas estruturas se você pode escrever trechos de formulários em HTML simples e então utilizar dicionários sobre a requisição para obter e manipular os dados?
O motivo é simples: você vai acabar escrevendo muito código repetitivo para coletar, validar e construir a resposta. Seria bem melhor definir os campos e os metadados de um formulário e reutilizá-los na forma de classes-base, aproveitando ao máximo as vantagens da orientação a objeto, ou seja, um framework de formulários.
O irmão mais velho desses frameworks chama-se CMFFormController -- que não é ruim, mas possui algumas desvantagens em relação ao formlib:
- Primeiro, o Form Controller espalha a lógica do formulário em vários arquivos, o que complica na hora de conhecê-la/acompanhá-la.
- Segundo, o Form Controller não permite a criação e exibição de widgets e, no caso de criá-los manualmente, seria insustentável utilizar campos do tipo escolha, multi-valorados.
- E por último, o Form Controler não trabalha com as interfaces de schema e as views do Zope 3. Utilizar um schema do Zope 3 pode ajudar na criação e edição de formulários.
Ainda assim, o Form Controller é útil e até mesmo preferível quando houver a necessidade de implementar um fluxo complexo de páginas ou, ainda, para personalizar alguns formulários mais antigos do Plone. Exemplo: o formulário de enviar a página para alguém.
A partir do Zope 2.9.3 (Plone 2.5), o zope.formlib é distribuído como parte do Zope 2. Como se trata de um pacote do Zope 3, é necessário o Five >= 1.4 para utilizá-lo dentro do Zope 2.
Onde colocar o código?
Você pode manter o código onde quiser: tudo em um único arquivo, uma classe por arquivo, em vários diretórios, como utilities em um arquivoutilities.py etc. Concentre-se apenas em duas coisas:
- Se você escrever partes do código (funções, classes) em arquivos separados, lembre-se de importá-las, como faria em qualquer programa em Python.
- Os registros na arquitetura de componentes devem ser declarados em um arquivo chamado configure.zcml na raiz do seu pacote, ou em outro diretório incluído a partir dele.
O autor optou por colocar todo o código deste tutorial em um único arquivo chamado browser.py para evitar confusão.
2. Criando um formulário simples de feedback
Esta seção demonstra como criar um formulário simples para feedbackO código gerado neste exemplo está disponível no pacote example.formlib.
Na prática, os componentes do formlib são componentes do tipo view do Zope 3, enriquecidas com algumas classes-base a fim de gerar saídas automaticamente baseadas em schemas e outras informações de configuração. Veremos isso em breve.
Primeiro, defina uma interface com o schema do formulário:
from zope.interface import Interface
from zope.schema import TextLine, Text
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True)
subject = TextLine(title=u'Subject',
required=True)
message = Text(title=u'Message',
description=u'The message body',
required=True)
A proposta dessa interface é documentar os campos do formulário. Defina cuidadosamente o tipo de cada campo no schema, pois o tipo de widget utilizado por padrão é determinado a partir dessa escolha. Para ver todos os campos disponíveis para o schema, leia a interface do pacote zope.schema.
Em seguida, instancie um formulário, que nada mais é do que uma classe que agrupa e ordena os campos e as ações. Para isso, crie uma subclasse herdando de PageForm, por sua vez proveniente do pacote Products.Five. Acrescente o código abaixo em seu produto:
from Products.Five.formlib.formbase import PageForm
Você precisará também utilizar uma espécie de híbrido entre templates do Zope 2 e do Zope 3:
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
O caminho mais simples para definir uma coleção de campos de formulário é utilizar o construtor de Fields encapsulando o schema definido anteriormente:
from zope.formlib import form
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
form_fields = form.Fields(IFeedbackForm)
Por herança da classe PageForm, a classe FeedbackForm possui funcionalidades do formlib. Por padrão, a classe PageForm gera todo o HTML que será exibido no formulário. Para isso, o formlib precisa saber quais campos são necessários. Esses campos são providos pelo atributo form_fields. O construtor Fields é uma classe auxiliar do formlib que gera os itens apropriados para cada campo a partir de schemas Zope 3 (nesse caso, o schema da interface definida acima).
Para fornecer um formulário completo, é necessário especificar uma ação para quando o formulário for enviado. Para definir a ação, utilize o decoratorform.action no método que manipulará os dados submetidos. Mais informações sobre ações em breve.
# use a dummy MailHost tool here to keep it simple
class MHost:
def __init__(self):
pass
def Send(self, sender, to, subject, body):
pass
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
form_fields = form.Fields(IFeedbackForm)
result_template = ViewPageTemplateFile('feedback_result.pt')
@form.action("send")
def action_send(self, action, data):
mhost = MHost()
self.mFrom = data['customer']
self.mTo = "feedback@mycompany.com"
self.mSubject = data['subject']
self.mBody = data['message']
mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
return self.result_template()
Aqui é onde seu trabalho realmente começa. Nesse exemplo, o page template feedback_result.pt é renderizado e retornado. Todos os atributos daview estarão disponíveis no template, que será exibido mais tarde.
<html metal:use-macro="context/@@standard_macros/view">
<head>
</head>
<body>
<div metal:fill-slot="body">
<h1 tal:content="view/label">Form label</h1>
<p>Thank you for your request about
<span tal:replace="view/mSubject">subject</span>,
<span tal:replace="view/mFrom">customer@mail</span>.</p>
<p>We will reply to it shortly.</p>
</div>
</body>
</html>
A biblioteca zope.formlib já inclui um template de formulário padrão, contendo as descrições dos campos, as estruturas dos widgets e os botões de envio, de forma que basta registrar o formulário com o código ZCML adequado para torná-lo acessível por um navegador. Assumindo que o código foi colocado no arquivo browser.py:
<browser:page
name="feedback"
for="Products.CMFPlone.Portal.PloneSite"
class=".browser.FeedbackForm"
permission="zope.Public"
/>
Vamos explicar o que significa esse trecho de ZCML:
- O atributo
forindica para que classe ou interface essa view estará disponível; nesse caso, apenas na raiz do site Plone. Para visualizar quais interfaces são fornecidas por um determinado objeto, acesse-o via ZMI e procure sua abaInterfaces.
- O atributo
nameseta o nome da view para que o formulário esteja disponível a partir de URL uma url.Ex.:http://<plone-site>/feedback.
- O atributo
classindica a classe da view responsável por renderizar a página do formulário; nesse caso, a classeFeedbackFormque está no arquivobrowser.py. - O atributo
permissionespecifica a permissão necessária para acessar a página.
Entre as permissões mais utilizadas temos:
zope.Public- sem restrições, acessível por todos.zope.View- permissão para visualizar o componente.zope.ManageContent- permissão para adicionar, editar e excluir objetos.
OBS: Os leitores mais atenciosos perceberão um nome especial no registro do novo componente de view: trata-se da diretiva <code>browser:page</code>. Essa tag XML emprega um namespace que precisa ser previamente declarado. Observe a linha em destaque abaixo:
<configure xmlns="http://namespaces.zope.org/zope" xmlns:browser="http://namespaces.zope.org/browser" xmlns:five="http://namespaces.zope.org/five">
E é isso. A página do formulário e a de confirmação deverão ser parecidas com:
Formulário de contato:

Confirmação:

3. Incluindo validação
A validação do formulário realizada no servidor da aplicação (server-side) é vital para garantir-se sobre os dados inseridos e proteger o site de usuários maliciosos.
Validação do campo
Uma vez compreendido o 'hello form', passaremos para um item mais avançado: validação.
A maneira mais fácil de administrar as validações de um formulário baseado em formlib é especificar as regras de validação em seu schema. De fato, você já implementou um início de validação: os campos customer, subject e message são obrigatórios. Isso quer dizer que se o formulário for enviado em branco, uma mensagem de erro (destacada em vermelho) será exibida informando sobre a necessidade de preenchimento do campo.
Vamos adicionar uma validação para o email informado no campo customer utilizando um atributo constraint no schema. Para simplificar, neste exemplo será utilizado o validador de email fornecido pelos utilitários do pacote CMFDefault, embora seja possível criar sua própria expressão regular para fazer a verificação. O argumento constraint receberá uma referência a uma função que retornará True se o valor enviado for válido, ou levantará uma exceção, herdada de zope.schema.ValidationError, retornando o valor contido na docstring do método.
from zope.schema import ValidationError
class InvalidEmailAddress(ValidationError):
"Invalid email address"
from Products.CMFDefault.utils import checkEmailAddress
from Products.CMFDefault.exceptions import EmailAddressInvalid
def validateaddress(value):
try:
checkEmailAddress(value)
except EmailAddressInvalid:
raise InvalidEmailAddress(value)
return True
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True,
constraint=validateaddress)
subject = TextLine(title=u'Subject',
required=True)
message = Text(title=u'Message',
description=u'The message body',
required=True)
Agora, caso um email inválido seja informado no campo customer e o botão send seja pressionado, uma mensagem de erro será exibida:

Fácil, não?
Validações fixas
O pacote zope.formlib também permite validações fixas de schemas, por exemplo, validar a inserção de um valor mínimo maior que o valor máximo permitido. No exemplo a seguir, o formulário será extendido para prover um conjunto de assuntos pré-determinados e um campo chamado other que deverá ser preenchido caso a opção Other seja selecionada na caixa de seleção Subject. É mais fácil visualizar em Python do que em palavras:
from zope.schema import Choice
from zope.interface import invariant, Invalid
class IFeedbackForm(Interface):
"""
A typical feedback schema
"""
customer = TextLine(title=u'Customer',
description=u'Customer email',
required=True,
constraint=validateaddress)
subject = Choice(title=u'Subject',
vocabulary='Available Subjects',
required=True,
)
other = TextLine(title=u'Other',
description=u"""
If you've specified Other above,
please fill this this field too.""",
required=False)
message = Text(title=u'Message',
description=u'The message body',
required=True)
@invariant
def otherFilledIfSelected(feedback):
if feedback.subject == u'Other' and not feedback.other:
raise Invalid("Please specify the motivation of your request")
O tipo do campo subject foi alterado para Choice e a lista de valores disponíveis foi configurada para receber os valores do vocabulário Available Subjects, uma utility nomeada que será definida mais à frente.
No momento de validação, o formulário chamará todas as funções decoradas com invariant no schema e tratará cada exceção do tipo Invalid que for levantada.
Defina agora o vocabulário Available Subjects:
from zope.schema.vocabulary import SimpleVocabulary
def availableSubjects(context):
subjects = ('Comment',
'Feature Request',
'Technical Issue',
'Complaint',
'Other',
)
return SimpleVocabulary.fromValues(subjects)
E registre-o como uma utility global no configure.zcml:
<configure ... >
...
<utility
component=".browser.availableSubjects"
name="Available Subjects"
provides="zope.schema.interfaces.IVocabularyFactory"
/>
</configure>
Reinicie a instância do Zope para que as alterações tomem efeito e teste seu novo formulário. Você verá algo similar a isto:

Infelizmente, as descrições de erros de validações fixas não são exibidas no template padrão.
4. Personalizando widgets e templates
Personalizando o template
O pacote plone.app.form provê um template padrão chamado pageform.pt que se integra bem com a skin padrão do Plone, mas você pode precisar personalizá-lo ou escrevê-lo do zero. Para isso, sobrescreva o atributo template presente na definição de classe do formulário:
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
template = ViewPageTemplateFile('feedback_form.pt')
result_template = ViewPageTemplateFile('feedback_result.pt')
@form.action("send")
def action_send(self, action, data):
mhost = MHost()
self.mFrom = data['customer']
self.mTo = "feedback@mycompany.com"
self.mSubject = data['subject']
self.mBody = data['message']
mhost.Send(self.mFrom, self.mTo, self.mSubject, self.mBody)
return self.result_template()
Como já foi dito, todos os atributos da view estarão disponíveis dentro do template, incluindo:
- label - O descritor exibido no topo do formulário.
- prefix - Uma string adicionada a todos os widgets e actions.
- form_fields - A listagem dos campos do formulário.
- widgets - A listagem das views dos campos do formulário. Os widgets são localizados como multi-adaptadores para cada campo do schema e o request, provendo as interfaces
IDisplayWidgetouIInputWidget. - errors - Lista dos erros encontrados durante a validação.
- error_views - A listagem das views para os erros encontrados. Essas views são localizadas como multi-adaptadores para cada erro e o request, provendo a interface
zope.app.form.browser.interfaces.IWidgetInputErrorView. - status - Variável de status do formulário, normalmente gerada a partir de gatilhos de sucesso ou falha.
- availableActions - Lista das ações diponíveis para o formulário.
- template - Template utilizado para visualizar o formulário.
É recomendado iniciar a partir do template padrão (pageform.pt) e ir personalizando-o aos poucos, cortando, colando, excluindo ou adicionando textos e/ou tags.
Utilizando templates nomeados
Templates nomeados são componentes do Zope 3 utilizados para indicar qual template será usado pelo formulário construído com zope.formlib. Nomear um template é um trabalho excessivo se você o tiver construído para trabalhar com a classe como um único componente. Porém, caso o template seja apenas um ajuste visual em cima de um comportamento que não será alterado, você pode querer personalizá-lo sem ter que tocar na implementação da classe, ou permitir a outros essa possibilidade. É exatamente isso que o Plone faz para substituir o template padrão do zope.formlib utilizando o pacoteplone.app.form.
Por favor observe que essa abordagem não foi utilizada como exemplo no produto example.formlib.
Templates nomeados são adaptadores da classe responsável pela view do formulário provendo a interface INamedTemplate, amarrados a tal classe somente pelos seus nomes na arquitetura de componentes. Dessa forma, um produto de terceiros (ex: um tema) pode registrar templates diferentes com o mesmo nome (normalmente em diferentes browser layers) para substituir o padrão. Além de tudo, são muito fáceis de usar. Modifique ou adicione as seguintes linhas na sua classe:
from zope.formlib.namedtemplate import NamedTemplate
# Five's ViewPageTemplateFile doesn't work correctly with formlib's NamedTemplateImplementation,
# so we use here the Plone implementation
from plone.app.form import named_template_adapter
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
template = NamedTemplate('feedback.form')
result_template = ViewPageTemplateFile('feedback_result.pt')
# rest of the form class implementation...
feedback_template = named_template_adapter(
ViewPageTemplateFile('feedback_form.pt'))
No configure.zcml, adicione o trecho abaixo para registrar o template nomeado como um adaptador para seu formulário:
<adapter
factory=".browser.feedback_template"
for=".browser.FeedbackForm"
name="feedback.form"
/>
Personalizando widgets
Como foi dito, os widgets do formulário são views para os campos do schemas, ou seja, multi-adaptadores das interfaces IDisplayWidget ouIInputWidget, dependendo se o widget estiver sendo usado para exibição dos dados do campo ou para editá-los, respectivamente.
custom_widget no campo (cujo padrão é None). Lembre como se constróem os campos do formulário:class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
# rest of the form class...
Os campos de form_fields são acessíveis diretamente por uma interface de dicionário, passando os campos do schema como chaves:
from zope.app.form.browser import RadioWidget as _RadioWidget
def RadioWidget(field, request):
vocabulary = field.vocabulary
widget = _RadioWidget(field, vocabulary, request)
return widget
class FeedbackForm(PageForm):
"""
A typical feedback form
"""
label = u'Contact Us'
form_fields = form.Fields(IFeedbackForm)
form_fields['subject'].custom_widget = RadioWidget
# rest of the form class...
Aqui especificamos um widget personalizado para o campo subject, RadioWidget, que por sua vez exibe uma caixa de botões de rádio, um para cada item do vocabulário do campo. Os pacotes zope.app.form.browser e plone.app.form.widgets fornecem um conjunto de widgets para utilizar e personalizar, incluindo dropdowns e TinyMCE/Kupu/WYSIWYG. Infelizmente, criar novos widgets não está no escopo deste tutorial.
A função RadioWidget merece uma pequena explanação. Acredite ou não, o zope.formlib não manipula corretamente widgets personalizados com vocabulários (chamados itens do widget), porque ele faz uma chamada ao form_field.custom_widget(campo, requisição) e ou o campo possui um vocabulário associado ou não, e os itens do widget também devem ser inicializados com um vocabulário; logo é necessária uma função de encapsulamento para corrigir esse problema.
O formulário melhorado deverá se parecer com:

