Chapter 12: Componentes e plugins

 Componentes e plugins

component
plugin

Componentes e plugins são recursos relativamente novos do web2py, e há algum desacordo entre os desenvolvedores sobre o que eles são e o que deveriam ser. A maior parte da confusão decorre dos diferentes usos desses termos em outros projetos de software e do fato de que os desenvolvedores ainda estão trabalhando para finalizar as especificações.

No entanto, o suporte a plugins é um recurso importante e precisamos fornecer algumas definições. Essas definições não pretendem ser definitivas, apenas coerentes com os padrões de programação que queremos discutir neste capítulo.

Vamos tentar resolver dois problemas aqui:

Como podemos construir aplicativos modulares que minimizam a carga do servidor e maximizam a reutilização de código? Como podemos distribuir pedaços de código de uma forma mais ou menos plug-and-play?

Componentes abordam a primeira questão; plugins endereçam o segundo.

 Componentes, LOAD e Ajax

load
LOAD
Ajax

Um componente é uma parte funcionalmente autônoma de uma página da web.

Um componente pode ser composto de módulos, controladores e visualizações, mas não há nenhum requisito estrito além de, quando incorporado em uma página da Web, estar localizado em uma tag html (por exemplo, um DIV, um SPAN ou um IFRAME) e ele deve executar sua tarefa independentemente do resto da página. Estamos especificamente interessados em componentes que são carregados na página e se comunicam com a função do controlador de componente via Ajax.

Um exemplo de um componente é um "componente de comentários" que está contido em um DIV e mostra comentários de usuários e um formulário de comentários pós-novos. Quando o formulário é enviado, ele é enviado ao servidor via Ajax, a lista é atualizada e o comentário é armazenado no banco de dados. O conteúdo DIV é atualizado sem recarregar o restante da página.

 LOAD

A função LOAD web2py torna isso fácil de ser feito sem conhecimento ou programação explícita de JavaScript/Ajax.

Nosso objetivo é ser capaz de desenvolver aplicativos web, reunindo componentes em layouts de página.

Considere um simples aplicativo web2py "test" que estende o aplicativo padrão de scaffolding com um modelo personalizado no arquivo "models/db_comments.py":

db.define_table('comment_post',
   Field('body', 'text', label='Your comment'),
   auth.signature)

uma ação em "controllers/comments.py"

@auth.requires_login()
def post():
    return dict(form=SQLFORM(db.comment_post).process(),
                comments=db(db.comment_post).select())

e as "visualizações/comentários/post.html" correspondentes

{{extend 'layout.html'}}
{{for post in comments:}}
<div class="post">
  On {{=post.created_on}} {{=post.created_by.first_name}}
  says <span class="post_body">{{=post.body}}</span>
</div>
{{pass}}
{{=form}}

Você pode acessá-lo como de costume em:

http://127.0.0.1:8000/test/comments/post

Até agora não há nada especial nesta ação, mas podemos transformá-lo em um componente definindo uma nova visão com a extensão ".load" que não estende o layout.

Por isso criamos um "views/comments/post.load":

{{for post in comments:}}
<div class="post">
  On {{=post.created_on}} {{=post.created_by.first_name}}
  says <blockquote class="post_body">{{=post.body}}</blockquote>
</div>
{{pass}}
{{=form}}

Nós podemos acessá-lo no URL

http://127.0.0.1:8000/test/comments/post.load

Este é um componente que podemos incorporar em qualquer outra página simplesmente fazendo

{{=LOAD('comments', 'post.load', ajax=True)}}

Por exemplo, em "controllers/default.py" podemos editar

def index():
    return dict()

e na vista correspondente, adicione o componente:

{{extend 'layout.html'}}
{{=LOAD('comments', 'post.load', ajax=True)}}

Visitando a página

http://127.0.0.1:8000/test/default/index

mostrará o conteúdo normal e o componente de comentários:

image

o {{=LOAD(...)}}  componente é processado da seguinte forma:

<script type="text/javascript"><!--
web2py_component("/test/comment/post.load", "c282718984176")
//--></script><div id="c282718984176">loading...</div>

(o código gerado de fato depende das opções passadas para a função LOAD).

o web2py_component(url, id)  função é definida em "web2py_ajax.html" e executa toda a magia: chama o url  via Ajax e incorpora a resposta para o DIV com correspondente id ; Ele captura todos os formulários enviados para o DIV e envia esses formulários via Ajax. O alvo do Ajax é sempre o próprio DIV.

 Assinatura LOAD

A assinatura completa do auxiliar LOAD é a seguinte:

LOAD(c=None, f='index', args=[], vars={},
     extension=None, target=None,
     ajax=False, ajax_trap=False,
     url=None, user_signature=False,
     timeout=None, times=1,
     content='loading...', **attr):

Aqui:

  • os dois primeiros argumentos c  e f  são o controlador e a função que queremos chamar respectivamente.
  • args  e vars  são os argumentos e variáveis que queremos passar para a função. O primeiro é uma lista, o último é um dicionário.
  • extension  é uma extensão opcional. Observe que a extensão também pode ser passada como parte da função como em f='index.load' .
  • target  é o id  do alvo DIV. Se não for especificado um alvo aleatório id  é gerado.
  • ajax  deve ser definido para True  se o DIV tiver que ser preenchido via Ajax e False  se o DIV tiver que ser preenchido antes que a página atual seja retornada (evitando assim a chamada do Ajax). Se definido para False , o código e a visão do componente serão executados no mesmo ambiente web2py do chamador.
  • ajax_trap=True  significa que qualquer envio de formulário no DIV deve ser capturado e enviado via Ajax, e a resposta deve ser renderizada dentro do DIV. ajax_trap=False  indica que os formulários devem ser enviados normalmente, recarregando a página inteira. ajax_trap  é ignorado e assumido como sendo True  E se ajax=True .
  • url , se especificado, substitui os valores de c , f , args , vars e extension  e carrega o componente no url . Ele é usado para carregar como páginas de componentes servidas por outros aplicativos (que podem ou não ser criados com o web2py). Note que usando url  assume ajax  Como True  sempre, porque web2py não pode saber de antemão se o componente é processado dentro de web2py ou é apenas uma página externa
  • user_signature  O padrão é Falso, mas, se você estiver logado, deve ser definido como Verdadeiro. Isso garantirá que o retorno de chamada do ajax seja assinado digitalmente. Isso está documentado no capítulo 4.
  • times  especifica quantas vezes o componente deve ser solicitado. Use "infinito" para continuar carregando o componente continuamente. Essa opção é útil para acionar rotinas regulares para uma determinada solicitação de documento.
  • timeout  define o tempo de espera em milissegundos antes de iniciar o pedido ou a freqüência se times  é maior que 1.
  • content  é o conteúdo a ser exibido durante a execução da chamada do ajax. Pode ser um ajudante como em content=IMG(..) .
  • opcional **attr  (atributos) podem ser passados para o conteúdo DIV .

Se não .load  vista é especificado, há uma generic.load  que processa o dicionário retornado pela ação sem layout. Funciona melhor se o dicionário contiver um único item.

Se você LOAD um componente com o .load  extensão e a função de controlador correspondente redireciona para outra ação (por exemplo, um formulário de login), o .load  extensão se propaga eo novo url (aquele a redirecionar também) também é carregado com um .load  extensão.

 Redirecionar de um componente

Para redirecionar de um componente, use isto:

redirect(URL(...), client_side=True)

mas observe que o URL redirecionado será o padrão para a extensão do componente. Veja notas sobre o argumento extension  dentro the URL function in Capítulo 4

 Recarregar página por meio de redirecionamento após o envio do componente

Se você chamar uma ação via Ajax e quiser que a ação force um redirecionamento da página pai, você pode fazer isso com um redirecionamento da função do controlador LOADed. Se você quiser recarregar a página pai, você pode redirecionar para ela. O URL pai é conhecido Client-Server component communications  )

então, depois de processar o envio do formulário, a função do controlador recarrega a página pai por meio do redirecionamento:

if form.process().accepted: 
    ...
    redirect(request.env.http_web2py_component_location, client_side=True)

Note que a seção abaixo, Client-Server component communications , descreve como o componente pode retornar o javascript, que pode ser usado para ações mais sofisticadas quando o componente é submetido. O caso específico de recarregar outro componente é descrito a seguir.

 Recarregar outro componente

Se você usar vários componentes em uma página, talvez queira enviar um componente para recarregar outro. Você faz isso obtendo o componente enviado para retornar algum javascript.

É possível codificar o DIV alvo, mas nessa receita usamos uma variável de string de consulta para informar ao controlador de envio qual componente queremos recarregar. É identificado pelo id do DIV que contém o componente de destino. Neste caso, o DIV possui o id 'map'. Note que é necessário usar target='map'  na CARGA do alvo; sem isso, o ID de destino é randomizado e o recarregamento () não funciona. Veja a assinatura LOAD acima.

Na visão, faça isso:

{{=LOAD('default', 'submitting_component.load', ajax=True, vars={'reload_div':'map'})}}

O controlador pertencente ao componente de envio precisa enviar o javascript de volta, portanto, adicione isso ao código do controlador existente ao processar o envio:

if form.process().accepted:
...
    if request.vars.reload_div:
        response.js =  "jQuery('#%s').get(0).reload()" % request.vars.reload_div

(Claro, remova o redirecionamento se você estivesse usando a abordagem da seção anterior).   É isso aí. As bibliotecas javascript do web2py cuidam do recarregamento. Isso poderia ser generalizado para lidar com vários componentes com javascript parecido com:

jQuery('#div1,#div2,#div3').get(0).reload()

Para obter mais informações sobre response.js, consulte Client-Server component communications  (abaixo).

 A postagem do Ajax não suporta formulários multipartes

Como a postagem do Ajax não suporta formulários com diversas partes, ou seja, uploads de arquivos, os campos de upload não funcionarão com o componente LOAD. Você pode se enganar pensando que funcionaria porque os campos de upload funcionarão normalmente se o POST for feito a partir da visualização .load do componente individual. Em vez disso, os uploads são feitos com widgets de terceiros compatíveis com ajax e comandos de armazenamento de upload manual do web2py.

 Comunicações de componente LOAD e Client-Server

Quando a ação de um componente é chamada via Ajax, o web2py passa dois cabeçalhos HTTP com o pedido:

web2py-component-location
web2py-component-element

que pode ser acessado pela ação através das variáveis:

request.env.http_web2py_component_location
request.env.http_web2py_component_element

Este último também é acessível via:

request.cid

request.cid

O primeiro contém o URL da página que chamou a ação do componente. Este último contém o id  do DIV que conterá a resposta.

A ação do componente também pode armazenar dados em dois cabeçalhos de resposta HTTP especiais que serão interpretados pela página inteira na resposta. Eles são:

web2py-component-flash
web2py-component-command

e eles podem ser definidos via:

response.headers['web2py-component-flash']='....'
response.headers['web2py-component-command']='...'

ou (se a ação for chamada por um componente) automaticamente via:

response.flash='...'
response.js='...'

O primeiro contém o texto que você deseja que seja exibido em resposta. Este último contém o código JavaScript que você deseja executar na resposta. Não pode conter novas linhas.

Como exemplo, vamos definir um componente de formulário de contato em "controllers/contact/ask.py" que permite ao usuário fazer uma pergunta. O componente enviará a pergunta por e-mail ao administrador do sistema, exibirá uma mensagem de "agradecimento" e removerá o componente da página:

def ask():
    form=SQLFORM.factory(
        Field('your_email', requires=IS_EMAIL()),
        Field('question', requires=IS_NOT_EMPTY()))
    if form.process().accepted:
        if mail.send(to='admin@example.com',
                  subject='from %s' % form.vars.your_email,
                  message = form.vars.question):
            response.flash = 'Thank you'
            response.js = "jQuery('#%s').hide()" % request.cid
        else:
            form.errors.your_email = "Unable to send the email"
    return dict(form=form)

As primeiras quatro linhas definem o formulário e aceitam-no. O objeto de correio usado para envio é definido no aplicativo de andaimes padrão. As últimas quatro linhas implementam toda a lógica específica do componente obtendo dados dos cabeçalhos de solicitação HTTP e definindo os cabeçalhos de resposta HTTP.

Agora você pode inserir este formulário de contato em qualquer página

{{=LOAD('contact', 'ask.load', ajax=True)}}

Observe que não definimos um .load  vista para o nosso ask  componente. Nós não temos que, porque ele retorna um único objeto (formulário) e, portanto, o "generic.load" vai fazer muito bem. Lembre-se de que as visualizações genéricas são uma ferramenta de desenvolvimento. Na produção, você deve copiar "views/generic.load" em "views/contact/ask.load".

user_signature
 
requires_signature
Podemos bloquear o acesso a uma função chamada via Ajax assinando digitalmente a URL usando o user_signature  argumento:

{{=LOAD('contact', 'ask.load', ajax=True, user_signature=True)}}

que adicionam uma assinatura digital ao URL. A assinatura digital deve então ser validada usando um decorador na função de retorno de chamada:

@auth.requires_signature()
def ask(): ...

 Links Ajax Presos e o Ajudante A

A
Ajax links

Normalmente, um link não é interceptado e, ao clicar em um link dentro de um componente, toda a página vinculada é carregada. Às vezes você quer que a página vinculada seja carregada dentro do componente. Isto pode ser conseguido usando o A  ajudante:

{{=A('linked page', _href='http://example.com', cid=request.cid)}}

E se cid  é especificado, a página vinculada é carregada via Ajax. o cid  é o id  do elemento html onde colocar o conteúdo da página carregada. Neste caso, configuramos para request.cid , ou seja, o id  do componente que gera o link. A página vinculada pode ser e geralmente é uma URL interna gerada usando o URL helper  .

 Plugins

Um plugin é qualquer subconjunto dos arquivos de um aplicativo.

e nós realmente queremos dizer "qualquer":

  • Um plugin não é um módulo, não é um modelo, não é um controlador, não é uma visão, mas pode conter módulos, modelos, controladores e/ou visualizações.
  • Um plugin não precisa ser funcionalmente autônomo e pode depender de outros plugins ou código de usuário específico.
  • Um plugin não é um sistema de plugins e portanto não tem nenhum conceito de registro nem isolamento, embora nós damos regras para tentar obter algum isolamento.
  • Estamos falando de um plug-in para seu aplicativo, não de um plug-in para web2py.

Então, por que é chamado de plugin ? Porque ele fornece um mecanismo para empacotar um subconjunto de um aplicativo e desempacotá-lo em outro aplicativo (por exemplo, plug-in ). De acordo com essa definição, qualquer arquivo no seu aplicativo pode ser tratado como um plug-in.

Quando o aplicativo é distribuído, seus plugins são empacotados e distribuídos com ele.

Na prática, o admin fornece uma interface para empacotar e descompactar plugins separadamente do seu aplicativo. Arquivos e pastas do seu aplicativo que possuem nomes com o prefixo plugin_ name pode ser agrupado em um arquivo chamado:

web2py.plugin. nome .w2p

e distribuídos juntos.

image

Qualquer arquivo pode ser parte de um plugin e esses arquivos não são tratados pelo web2py de forma diferente de outros arquivos. Exceto que arquivos e pastas em que têm um plugin_  prefixo são reconhecidos por admin e garoupa juntos por admin de acordo com o nome do postfix. admin trata-os de forma diferente, não web2py.

image

Na prática, nos preocuparemos apenas com dois tipos de plugins:

  • Plugins de componentes . Estes são plugins que contêm componentes, conforme definido na seção anterior. Um plug-in de componente pode conter um ou mais componentes. Podemos pensar, por exemplo, em um plugin_comments  que contém o componente comentários proposto acima. Outro exemplo poderia ser plugin_tagging  que contém um componente tagging e um componente tag-cloud que compartilham algumas tabelas do banco de dados também definidas pelo plugin.
  • Plugins de Layout . Estes são plugins que contêm uma visualização de layout e os arquivos estáticos requeridos por tal layout. Quando o plug-in é aplicado, ele dá ao aplicativo uma nova aparência.

Pelas definições acima, os componentes criados na seção anterior, por exemplo "controllers/contact.py", já são plugins. Podemos movê-los de um aplicativo para outro e usar os componentes que eles definem. No entanto, eles não são reconhecidos como tal por admin porque não há nada que os rotule como plugins. Então, há dois problemas que precisamos resolver:

  • Nomeie os arquivos do plug-in usando uma convenção, para que admin pode reconhecê-los como pertencentes ao mesmo plugin
  • Se o plug-in tiver arquivos de modelo, estabeleça uma convenção para que os objetos definidos não poluam o espaço de nomes e não entrem em conflito entre si.

Vamos supor que um plugin seja chamado de name . Aqui estão as regras que devem ser seguidas:

Regra 1: Modelos e controladores de plugins devem ser chamados, respectivamente

  • models/plugin_ nome .py
  • controllers/plugin_ nome .py

e exibições de plug-in, módulos, arquivos estáticos e privados devem estar em pastas chamadas, respectivamente:

  • views/plugin_ nome /
  • modules/plugin_ nome /
  • static/plugin_ nome /
  • private/plugin_ nome /

Regra 2: Os modelos de plug-in só podem definir objetos com nomes que começam com

  • plugin_ nome
  • Plugin Nome
  • _

Regra 3: Os modelos de plug-in só podem definir variáveis de sessão com nomes que começam com

  • session.plugin_ nome
  • session.Plugin Nome

Regra 4: Os plugins devem incluir licença e documentação. Estes devem ser colocados em:

  • static/plugin_ nome /license.html
  • static/plugin_ nome /about.html

Regra 5: O plugin só pode confiar na existência dos objetos globais definidos no scaffolding "db.py", ou seja,

  • uma conexão de banco de dados chamada db
  • a Auth  instância chamada auth
  • uma Crud  instância chamada crud
  • uma Service  instância chamada service

Alguns plugins podem ser mais sofisticados e ter um parâmetro de configuração no caso de existir mais de uma instância de db.

Regra 6: Se um plugin precisar de parâmetros de configuração, estes devem ser configurados através de um PluginManager, conforme descrito abaixo.

PluginManager

Seguindo as regras acima, podemos garantir que:

  • admin reconhece todo o plugin_ 'nome' 'arquivos e pastas como parte de uma única entidade.
  • plugins não interferem uns com os outros.

As regras acima não resolvem o problema das versões e dependências do plugin. Isso está além do nosso escopo.

 Plugins de componentes

component plugin

Plugins de componentes são plugins que definem componentes. Componentes geralmente acessam o banco de dados e definem com seus próprios modelos.

Aqui nos voltamos a anterior comments  componente em um comments_plugin  usando o mesmo código que escrevemos antes, mas seguindo todas as regras anteriores.

Primeiro, criamos um modelo chamado "models/plugin_comments.py":

db.define_table('plugin_comments_comment',
   Field('body', 'text', label='Your comment'),
   auth.signature)

def plugin_comments():
    return LOAD('plugin_comments', 'post', ajax=True)

(note que as duas últimas linhas definem uma função que simplificará a incorporação do plugin)

Em segundo lugar, definimos um "controllers/plugin_comments.py"

def post():
    if not auth.user:
        return A('login to comment', _href=URL('default', 'user/login'))
    comment = db.plugin_comments_comment
    return dict(form=SQLFORM(comment).process(),
                comments=db(comment).select())

Em terceiro lugar, criamos uma visualização chamada "views/plugin_comments/post.load":

{{for comment in comments:}}
<div class="comment">
  on {{=comment.created_on}} {{=comment.created_by.first_name}}
  says <span class="comment_body">{{=comment.body}}</span>
</div>
{{pass}}
{{=form}}

Agora podemos usar admin para embalar o plugin para distribuição. Admin irá salvar este plugin como:

web2py.plugin.comments.w2p

Podemos usar o plug-in em qualquer visualização simplesmente instalando o plugin através do editar página em admin e adicionando isso às nossas próprias visões

{{=plugin_comments()}}

É claro que podemos tornar o plugin mais sofisticado, tendo componentes que levam parâmetros e opções de configuração. Quanto mais complexos os componentes, mais difícil se torna evitar colisões de nomes. O gerenciador de plug-ins descrito abaixo foi projetado para evitar esse problema.

 Gerenciador de plugins

o PluginManager  é uma classe definida em gluon.tools . Antes de explicar como funciona dentro, vamos explicar como usá-lo.

Aqui nós consideramos o anterior plugin_comments  e nós fazemos melhor. Queremos poder personalizar:

db.plugin_comments_comment.body.label

sem ter que editar o próprio código do plugin.

Aqui está como podemos fazer isso:

Primeiro, reescreva o plugin "models/plugin_comments.py" desta forma:

def _():
    from gluon.tools import PluginManager
    plugins = PluginManager('comments', body_label='Your comment')

    db.define_table('plugin_comments_comment',
        Field('body', 'text', label=plugins.comments.body_label),
        auth.signature)
    return lambda: LOAD('plugin_comments', 'post.load', ajax=True)
plugin_comments = _()

Observe como todo o código, exceto a definição da tabela, é encapsulado em uma única função chamada _  para que não polua o namespace global. Observe também como a função cria uma instância de um PluginManager .

Agora, em qualquer outro modelo em seu aplicativo, por exemplo, em "models/db.py", você pode configurar este plug-in da seguinte forma:

from gluon.tools import PluginManager
plugins = PluginManager()
plugins.comments.body_label = T('Post a comment')

o plugins  objeto já está instanciado no aplicativo padrão de scaffolding em "models/db.py"

O objeto PluginManager é um objeto de armazenamento singleton de nível de thread de objetos Storage. Isso significa que você pode instanciar quantos você quiser dentro do mesmo aplicativo, mas (se eles têm o mesmo nome ou não) eles agem como se houvesse uma única instância do PluginManager.

Em particular, cada arquivo de plug-in pode criar seu próprio objeto PluginManager e registrar-se e seus parâmetros padrão com ele:

plugins = PluginManager('name', param1='value', param2='value')

Você pode sobrescrever estes parâmetros em outro lugar (por exemplo, em "models/db.py") com o código:

plugins = PluginManager()
plugins.name.param1 = 'other value'

Você pode configurar vários plugins em um só lugar.

plugins = PluginManager()
plugins.name.param1 = '...'
plugins.name.param2 = '...'
plugins.name1.param3 = '...'
plugins.name2.param4 = '...'
plugins.name3.param5 = '...'

Quando o plug-in é definido, o PluginManager deve receber argumentos: o nome do plug-in e os argumentos nomeados opcionais, que são os parâmetros padrão. No entanto, quando os plugins são configurados, o construtor PluginManager não deve receber argumentos. A configuração deve preceder a definição do plug-in (isto é, deve estar em um arquivo de modelo que vem primeiro em ordem alfabética).

 Plugins de layout

layout plugin

Os plug-ins de layout são mais simples que os plug-ins de componente porque geralmente não contêm código, mas apenas exibições e arquivos estáticos. No entanto, você ainda deve seguir uma boa prática:

Primeiro, crie uma pasta chamada "static/plugin_layout _ name /" (onde name é o nome do seu layout) e coloque todos os seus arquivos estáticos lá.

Segundo, crie um arquivo de layout chamado "views/plugin_layout _ name /layout.html" que contenha seu layout e vincule as imagens, os arquivos CSS e JavaScript em "static/plugin_layout _ name /"

Terceiro, modifique o "views/layout.html" para que ele simplesmente leia:

{{extend 'plugin_layout_name/layout.html'}}
{{include}}

O benefício deste design é que os usuários deste plugin podem instalar vários layouts e escolher qual deles aplicar simplesmente editando "views/layout.html". Além disso, "views/layout.html" não será embalado por admin juntamente com o plug-in, não há risco de que o plug-in substitua o código do usuário no layout instalado anteriormente.

 Repositórios de plugins, plugins de instalação via admin

Embora não haja um único repositório de plug-ins web2py, você pode encontrar muitos deles em um dos seguintes URLs:

http://web2pyslices.com (this is the leading repository and is integrated to the web2py admin application for one-click installs)

http://web2py.com/plugins
http://web2py.com/layouts

Versões recentes do admin do web2py permitem a busca e a instalação automáticas de plugins a partir de web2pyslices. Para adicionar um plug-in a um aplicativo, edite-o por meio do aplicativo de administração e escolha Download de plug-ins, atualmente na parte inferior da tela.

Para publicar seus próprios plugins, crie uma conta em web2pyslices.

Aqui está uma captura de tela mostrando alguns dos plugins auto-instaláveis:

image

 top