[SP-pm] Catalyst: Começando...
Thiago Rondon
thiago at aware.com.br
Wed Feb 4 04:36:29 PST 2009
Sim, para um aplicativo real, esta seria uma boa maneira de aplicar o
Chained. Que tal um documento
sobre praticas de utilização de URI/Chained com o Catalyst ?
Mais um pouco, você poderia transformar um texto para adicionarmos no
wiki também !
No exemplo que coloquei no texto, é realmente pratico, fast-food, não vi
necessidade de criar um base
no Root.pm.
-Thiago Rondon
Lindolfo "Lorn" Rodrigues escreveu:
> Você pode mudar isso para Chained Action:
>
> sub index :Path :Args(0) {
> my ($self, $c) = @_;
>
> $c->res->redirect(
> $c->uri_for(
> $c->controller->action_for('list')
> )
>
> );
> }
>
> sub root : Chained('base') Args(0) {
> my ($self, $c) = @_;
> $c->res->redirect(
> $c->uri_for(
> $c->controller->action_for('list')
> )
> );
> }
>
>
> # Essa parte do Controller Telefone, pode ficar assim:
>
> - sub base :Chained('/') CaptureArgs(0) PathPart('telefone') {
> + sub base :Chained('/base') CaptureArgs(0) PathPart('telefone') {
>
>
> Para isso preciso disso no Controller Root.pm, eu acho que assim fica mais "extensivel" sua agenda.
>
> Root.pm
>
> sub base : Chained('/') CaptureArgs(0) PathPart('') {}
>
> sub root : Chained('base') PathPart('') Args(0){
>
> my ($self, $c) = @_;
> # Quando o usuario acessar localhost:3000/ vai ser redirecionado para localhost:3000/telefone, pois é
> # a unica parte da agenda que está pronta
> $c->res->redirect('/telefone');
>
> }
>
> Usando Chained Action a partir do Root.pm, você consegue controlar coisas desse tipo:
>
> sub favicon : Chained('base') PathPart('favicon.ico') Args(0) {
> my ($self, $c) = @_;
> # coloque aqui o codigo para servir o favicon.ico de sua prefrencia
>
> }
>
> sub error_404 : Chained('base') PathPart('') Args {
> my ($self, $c) = @_;
> $c->response->body( 'Page not found' );
> $c->response->status(404);
>
>
>
> }
>
> O CODIGO ACIMA NÃO FOI TESTADO :)
>
>
> 2009/2/4 Thiago Rondon <thiago em aware.com.br <mailto:thiago em aware.com.br>>
>
>
> Espero continuações... oops, contribuições. :)
>
> -Thiago Rondon
>
> Blabos de Blebe escreveu:
>
> Muito bom. Gostei das explicações nos comentários. Vai ter
> continuação? Espero que sim.
>
> Parabéns
>
> 2009/2/3 Daniel de Oliveira Mantovani
> <daniel.oliveira.mantovani em gmail.com
> <mailto:daniel.oliveira.mantovani em gmail.com>>:
>
>
> Valeu Thiago!
> 2009/2/3 Nelson Ferraz <nferraz em gmail.com
> <mailto:nferraz em gmail.com>>
>
>
> Parece muito bom, Thiago! Obrigado pela contribuição!
>
> Por que não formata o documento em pod, e copia para o
> wiki? Aqui vai um
> começo:
>
> =head1 NOME
>
> Catalyst - O Elegante framework de programação Web em
> Perl.
>
> =head1 DESCRICAO
>
> Catalyst é um framework elegante de programação MVC
> para Web em Perl
> (ambos gratuitos e de código aberto). Este tutorial
> simples e talvez muito objetivo tem como missão
> facilitar o
> entedimento para os novos nesta ferramenta de forma
> pratica, não
> se prendendo muito a conceitos, motivos e paradigmas.
>
> =head1 INICIO
>
> =head2 Instalação do Catalyst.
>
> O Perl tem o CPAN, no qual é um repositorio farto de
> modulos e
> documentação, você pode utilizar o site
> http://search.cpan.org/ para
> navegar por ele, e toda distribuição do Perl vem com
> um aplicativo
> chamado "cpan" para você poder instalar os modulos
> atravez da
> linha de comando.
>
> $ export PERL_MM_USE_DEFAULT=1
> $ cpan -i Task::Catalyst
>
> O Catalyst tem algumas dependencias, e por isto, além
> do pacote do
> Catalyst o "cpan" irá automaticamente instalar as suas
> dependencias,
> perceba que estamos utilizando o pacote
> Task::Catalyst, no qual contém
> uma serie de pacotes recomendadas para a utilização do
> Catalyst.
>
> DICA: Sete a variavel de ambiente
> PERL_MM_USE_DEFAULT=1 para o cpan
> não lhe questionar sobre as dependencias e demais
> perguntas, e
> instalar elas sem lhe perguntar.
>
> Pronto, a instalação esta concluida, agora estamos com
> o sistema apto
> para programar utilizando o framework de programação
> Catalyst.
>
> =head2 Iniciando o meu primeiro projeto.
>
> No pacote do Catalyst, vem acompanhando um aplicativo
> que facilita a
> nossa vida para montar o ambiente básico para
> iniciarmos o nosso
> projeto, chamado de "catalyst.pl", para isto basta
> executar ele com o
> primeiro argumento sendo o nome do projeto.
>
> $ catalyst.pl webapp
> created "webapp"
> created "webapp/script"
> created "webapp/lib"
> created "webapp/root"
> created "webapp/root/static"
> created "webapp/root/static/images"
> created "webapp/t"
> created "webapp/lib/webapp"
> created "webapp/lib/webapp/Model"
> created "webapp/lib/webapp/View"
> created "webapp/lib/webapp/Controller"
> created "webapp/webapp.conf"
> created "webapp/lib/webapp.pm <http://webapp.pm>"
> created "webapp/lib/webapp/Controller/Root.pm"
> created "webapp/README"
> created "webapp/Changes"
> created "webapp/t/01app.t"
> created "webapp/t/02pod.t"
> created "webapp/t/03podcoverage.t"
> created "webapp/root/static/images/catalyst_logo.png"
> created "webapp/root/static/images/btn_120x50_built.png"
> created
> "webapp/root/static/images/btn_120x50_built_shadow.png"
> created
> "webapp/root/static/images/btn_120x50_powered.png"
> created
> "webapp/root/static/images/btn_120x50_powered_shadow.png"
> created "webapp/root/static/images/btn_88x31_built.png"
> created
> "webapp/root/static/images/btn_88x31_built_shadow.png"
> created
> "webapp/root/static/images/btn_88x31_powered.png"
> created
> "webapp/root/static/images/btn_88x31_powered_shadow.png"
> created "webapp/root/favicon.ico"
> created "webapp/Makefile.PL"
> created "webapp/script/webapp_cgi.pl"
> created "webapp/script/webapp_fastcgi.pl"
> created "webapp/script/webapp_server.pl"
> created "webapp/script/webapp_test.pl"
> created "webapp/script/webapp_create.pl"
>
> Pronto, agora estamos com nosso ambiente básico para
> começar a
> programar, vou explicar de forma rápida a estrutura
> de diretorios criada para você entender melhor:
>
> $ ls webapp/
> Changes lib/ Makefile.PL README root/ script/
> t/ webapp.conf
>
> Veja, que temos quatro diretorios, que são eles:
>
> =over
>
> =item lib/
>
> Contém a parte da programação lógica, onde iremos
> escrever nossos
> modulos e códigos em Perl.
>
> =item root/
>
> Neste diretorio iremos colocar arquivos estáticos,
> como templates,
> javascript e css.
>
> =item script/
>
> Scripts para inicializar a nossa aplicação.
>
> =item t/
>
> Testes para nossa aplicação.
>
> =back
>
> Vamos falar um pouco do diretorio "script/", nele você
> tem um script
> que usaremos muito neste tutorial, webapp_server.pl, no
> qual irá iniciar um servidor http na porta 3000 por
> padrão, para
> visualizarmos o nosso aplicativo:
>
> $ script/webapp_server.pl
>
> Depois de executar ele, você já pode acessar o
> aplicativo pelo seu
> browser favorito, pelo endereço:
>
> L<http://localhost:3000>
>
> =head2 Entendendo um MVC (Model-View-Controller)
>
> MVC é um padrão de arquitetura de software, uma das
> maiores
> dificuldades do entendimento de um framework de
> programação
> web como o Catalyst para os iniciantes é justamente
> este conceito.
>
> Basicamente o significado deles é:
>
> =over
>
> =item Model:
>
> São as informações utilizadas pelo aplicativo, no qual
> este modelo
> pode ser um banco de dados, um arquivo texto e etc.
>
> =item Controller:
>
> Lógica do nosso aplicativo, onde podemos por exemplo
> alterar as
> informações (Model), filtrar e etc.
>
> =item View:
>
> Renderizar o Model e o Controller para o usuário, ou
> seja como o
> aplicativo será apresentado.
>
> =back
>
> Esta é uma forma rápida de apresentar os três
> componentes do MVC,
> sabendo disto, vamos começar a produzir nosso aplicativo.
>
> =head2 Nosso projeto: Agenda telefonica
>
> Vamos começar a estudar o nosso aplicativo, ele irá
> gerenciar uma
> lista de telefones, no qual iremos efetuar um simples
> "CRUD"
> (Create-Remove-Update-Destroy), no qual iremos
> permitir efetuar a
> criação, remover, atualizar e apagar dados de nossa lista.
>
> Vamos projetar nossa agenda telefonica, utilizando o
> MVC da seguinte da
> forma:
>
> =over
>
> =item Model:
>
> Iremos utilizar o MySQL, porém para efetuar a
> conectividade com o
> banco de dados, iremos utilizar um driver do DBI, no
> qual o MySQL e tantos outros ele suporta, e é
> interessante além de
> utilizar o driver de comunicação com o banco de dados,
> algum
> ORM
> (http://en.wikipedia.org/wiki/Object-relational_mapping)
> para
> facilitar a interface com o DB, existem alguns
> disponiveis como
> por exemplo o Class::DBI ou DBIx::Class para evitar
> que você escreva
> SQL no teu código, evitar alguns bugs e trazer
> agilidade no
> desenvolvimento, no nosso caso iremos utilizar o
> DBIx::Class.
>
> =item View:
>
> Iremos utilizar o HTML como formato de apresentação
> para nossos
> usuarios, portanto teremos que carregar o modulo que
> cuidará de manipular os dados que iremos jogar em
> nosso arquivo de
> template, existem alguns modulos disponiveis como o
> HTML::Manson ou Template::Toolkit), iremos utilizar
> neste caso o
> Template::Toolkit.
>
> =item Controller:
>
> Aqui é o lugar onde iremos programar, captar os dados
> no Model e
> "espirrar" para a View, entre outras.
>
> =back
>
> =head2 Começando pelo Model.
>
> Bem, já dissemos que iremos utilizar o MySQL como
> banco de dados, e
> também já explicamos por que iremos utilizar o
> DBIx::Class.
>
> Precisamos instalar ele:
>
> $ cpan -i DBIx::Class
>
> Certo, agora temos tudo o que precisamos para
> trabalhar com o nosso
> Model. Neste tutorial eu vou utilizar o exemplo de criar a
> tabela direto pelo client do MySQL, e depois ler elas
> pelo DBIx::Class.
>
> mysql> create database webapp
> mysql> grant all on webapp.* to webapp em localhost
> identified by
> 'webapp';
> mysql> flush privileges;
> mysql> create table telefones (
> id int not null auto_increment,
> nome varchar(255),
> telefone varchar(255),
> primary key(id));
>
> Criado a nosso database e nossa tabela, agora vamos
> para "importar"
> nosso banco de dados para o DBIx::Class, eu não irei me
> aprofundar no DBIx::Class neste tutorial, pois não é o
> objetivo dele.
>
> Para importarmos este database para os schemas que o
> DBIx::Class
> utiliza, iremos executar um dos scripts facilitadores
> do diretorio
> scripts/ que se chama "webapp_create.pl" no qual
> facilita algumas
> tarefas como esta, a sintaxe utilizada é
> auto-explicativa, é
> basicamente:
>
> # script/webapp_create.pl model DB DBIC::Schema
> webapp::Schema
> create=static dbi:mysql:dbname=webapp webapp webapp
> exists "/home/thiago/webapp/script/../lib/webapp/Model"
> exists "/home/thiago/webapp/script/../t"
> Dumping manual schema for webapp::Schema to directory
> /home/thiago/webapp/script/../lib ...
> Schema dump completed.
> created
> "/home/thiago/webapp/script/../lib/webapp/Model/DB.pm"
> created "/home/thiago/webapp/script/../t/model_DB.t"
>
> Vejamos os argumentos deste script:
>
> =over
>
> =item model
>
> Estamos criando um model para o script webapp_create.pl
>
> =item DB
>
> O nome criado para este modem será DB (vide
> lib/webapp/Model/DB.pm)
>
> =item DBIC::Schema
>
> Nome do helper para criar o Schema para o DBIx::Class
>
> =item create=static
>
> Significa que o schema será estatico, ou seja, a toda
> alteração nas
> tabelas, você precisa atualizar nos arquivos criados
> para o schema.
>
> =item dbname
>
> Nome do database no MySQL.
>
> =item webapp
>
> Usuario para se conectar no banco.
>
> =item webapp
>
> Senha para se conectar no banco.
>
> =back
>
> Agora, de uma lida nos arquivos criados em
> lib/webapp/Schema/* e
> lib/webapp/Model/*
>
> Pronto, já temos o nosso Model pronto para o nosso
> aplicativo.
>
> =head2 View - Template Toolkit.
>
> Iremos utilizar neste exemplo a Template::Toolkit,
> iremos utilizar um
> helper da mesma maneira que
> usamos para o banco de dados:
>
> # script/webapp_create.pl view TT TT
> exists "/home/thiago/webapp/script/../lib/webapp/View"
> exists "/home/thiago/webapp/script/../t"
> created
> "/home/thiago/webapp/script/../lib/webapp/View/TT.pm"
> created "/home/thiago/webapp/script/../t/view_TT.t"
>
> Pronto, o helper já criou os arquivos necessarios para
> utilizarmos a
> Template Toolkit em nosso projeto.
>
> =head2 Controller - Agora sim, programando!
>
> Estamos na melhor parte, é onde tudo acontece, iremos
> criar 4 actions,
> para criar, remover, listar e editar.
>
> Eu irei colocar o código que utilizei, e com ele
> comentado explicando
> passo-a-passo do que estou
> fazendo nele. Não irei utilizar nenhum helper ou
> facilitador nesta
> parte, pois acredito que a melhor maneira
> de se desenvolver o Controller é na 'unha'.
>
> Iremos criar um arquivo
> lib/webapp/Controller/telefone.pm
> <http://telefone.pm>, no qual ele
> irá representar na URI "/telefone".
>
> Caso queira baixar todos os arquivos, clique aqui.
>
> package webapp::Controller::telefone;
>
> use strict;
> use warnings;
> use parent 'Catalyst::Controller';
>
> # Nos estamos utilizando um mecanismo muito
> interessante neste
> # exemplo, que recomendo a utilizacao, o Chained.
> # Não iremos entrar discutir sobre ele, porem ele ajuda
> # manusearmos os argumentos passados pela URI.
> # Por tanto, vamos criar uma rotina "base" no qual
> todas as
> # requisições passaram por ela (Chained()).
> #
> search.cpan.org/dist/Catalyst-Runtime/lib/Catalyst/DispatchType/Chained.pm
> <http://search.cpan.org/dist/Catalyst-Runtime/lib/Catalyst/DispatchType/Chained.pm>
>
> sub base :Chained('/') CaptureArgs(0)
> PathPart('telefone') {
> my ($self, $c) = @_;
>
> # stash é o local onde iremos colocar
> informações para compartilhar
> # com outras partes do nosso aplicativo, para
> cada requisição o
> stash
> # tem uma vida.
> # No caso, abaixo estamos atribuindo ao
> "collection" no stash o
> nosso
> # model "DB::telefones", que corresponde a nossa
> tabela telefones
> que
> # criamos no MySQL.
>
> $c->stash->{collection} =
> $c->model('DB::telefones');
> }
>
>
> # Veja, aqui estamos criando uma URI list/, no qual
> ela esta
> vinculada a nossa
> # função base, criada anteriormente (Chained('base')),
> #
> # Ou seja, para chamar a URL,
> http://localhost:3000/telefone/list,
> primeiramente
> # ele vai passar na base e depois na list. (* E por
> último, neste
> nosso exemplo
> # na action end do Root.pm, no qual nem iremos
> comentar neste
> tutorial).
> #
> # Veja que estamos dizendo Args(0), isto significa
> que não preciso de
> nenhum
> # elemento extra na minha URI, sempre que esta
> action for requisitada,
> será
> # /telefone/list
>
> sub list :Chained('base') Args(0) {
> my ($self, $c) = @_;
>
> # "req" ou "request" são as informações que
> estão no request da
> # requisição, e no qual temos o metodo param que
> retorna o valor
> # do atributo que gostariamos.
> my $key = $c->req->param('key') || "";
>
> # Aham, aqui estamos utilizando o DBIx:Class,
> repare que não iremos
> # escrever nenhum um código de SQL aqui, veja
> abaixo que iremos
> # utilizar uma função search_like(), no qual
> iremos procurar pelo
> # elemento $key nos dois campos (nome,
> telefone), estou usando o
> # $key, caso o usuario queira procurar por
> alguma palavra chave.
> my $items = $c->stash->{collection}->search_like({
> nome => "\%$key\%",
> telefone => "\%$key\%"
> });
>
> # Aqui estou colocando no stash o key, para
> mostrar na View por
> qual
> # palavra o usuario esta procurando, e o items
> encontrados.
> $c->stash->{key} = $key;
> $c->stash->{items} = $items;
> }
>
> # Esta função é para remover uma determinada coluna
> do nosso banco de
> dados,
> # repare que aqui estamos utilizando Args(1), ou
> seja estou esperando
> um
> # elemento na minha URI, além de /telefones/destroy,
> eu só irei acessar
> esta
> # URI, se acessar por /telefones/destroy/N, onde N é
> o ID do nosso
> item.
> # Veja que no código da função, estaremos novamente
> usando uma
> função do DBIx:Class,
> # primeiramente irei procurar por este elemento com
> find() e então
> irei remover
> # ele.
>
> sub destroy :Chained('base') :Args(1) {
> my ($self, $c, $id) = @_;
> my $row = $c->stash->{collection}->find({ id =>
> $id });
> $row->delete if $row;
> }
>
> # Aqui, iremos editar o nosso item, onde também
> esperamos um
> argumento, que é o id
> # do item, e iremos procurar ele.
> # Repare que aqui, eu faço uma verificação, onde
> quero saber se o
> metodo utilizado
> # para chamar esta requisição é POST, caso não seja
> eu vou
> "encarrar" (detach())
> # esta função e vou passar para a próxima função da
> cadeia. (end()
> no Root.pm, neste
> # caso).
> # Resumindo, se o usuário não apertou "ALTERAR" no
> html (no qual o
> form esta enviando
> # os dados via POST), eu irei mostrar a página com
> os dados do
> objeto N ($id).
>
> sub edit :Chained('base') :Args(1) {
> my ($self, $c, $id) = @_;
>
> my $row = $c->stash->{row} =
> $c->stash->{collection}->find({id =>
> $id});
> $c->stash->{template} = "telefone/create.tt
> <http://create.tt>";
>
> $c->detach() unless $c->req->method eq 'POST';
>
> # Caso o usuario, tenha apertado o submit do
> nosso form, para
> alterar os dados,
> # eu vou colocar eles em $parameters = {}, e vou
> mandar o $row
> (que é o objeto
> # do find()) para o metodo update().
>
> my $parameters;
> $parameters->{nome} = $c->req->param('nome');
> $parameters->{telefone} =
> $c->req->param('telefone');
>
> $row->update($parameters);
>
> # Agora, nao vou mandar ele para a mesma tela,
> vou enviar para
> uma template diferente.
> $c->stash->{template} = "telefone/edit_ok.tt
> <http://edit_ok.tt>";
> }
>
> # Esta rotina é bem parecida com a do edit, com a
> unica diferença,
> que ao invez de atualizar
> # eu vou inserir.
>
> sub create :Chained('base') :Args(0) {
> my ($self, $c) = @_;
> $c->detach() unless $c->req->method eq 'POST';
>
> my $parameters;
> $parameters->{nome} = $c->req->param('nome');
> $parameters->{telefone} =
> $c->req->param('telefone');
>
> my $row = $c->stash->{collection}->new($parameters);
> $row->insert;
>
> $c->stash->{template} = "telefone/create_ok.tt
> <http://create_ok.tt>";
> }
>
> # Caso, eu não especifique nada na URI de telefones,
> ou seja, acessar
> # http://localhost:3000/telefone eu irei
> redirecionar para a action
> list,
> # que já comentamos sobre ela.
> sub index :Path :Args(0) {
> my ($self, $c) = @_;
>
> $c->res->redirect(
> $c->uri_for(
> $c->controller->action_for('list')
> )
> );
> }
>
> 1;
>
>
> Código pronto, porem precisamos das templates.
>
> Agora, crie estes arquivos no diretorio root/telefone:
>
> Este arquivo, é para a action "create", no qual é o
> nosso formulario
> para criar um item na nossa agenda.
>
> =head3 create.tt <http://create.tt>
>
> <h1>Criar</h1>
>
> <form method="POST">
>
> <table><tr><td>
> Nome:
> </td><td> <input type="text" name="nome" value="[%
> row.nome %]"
> size=20>
> </td></tr><tr><td>
> Telefone:
> </td><td> <input type="text" name="telefone" value="[%
> row.telefone %]" size=20>
> </td></tr><tr><td colspan="2">
> <center><input type="submit"></center>
> </td></tr></table>
>
> </form>
>
> Este arquivo é a mensagem depois da criação do item.
>
> =head3 create_ok.tt <http://create_ok.tt>
>
> <h1>Telefone inserido com sucesso!</h1>
>
> Mensagem para o item apagado.
> =head3 destroy.tt <http://destroy.tt>
>
> <h1>Telefone apagado</h1>
>
> Mensagem para item editado.
>
> =head3 edit_ok.tt <http://edit_ok.tt>
>
> <h1>Editado com sucesso.</h1>
>
> Listar itens da nossa agenda, e também dar ao usuário
> a opção de procurar
> neles.
>
> =head3 list.tt <http://list.tt>
>
> <h1>Listar</h1>
>
> <form>
> Procurar por:
> <input type="text" name="key" size="15">
> <input type="submit" value=" Procurar " >
> </form>
>
> [% IF key %]
> <br/>
> <b>Procurando por [% key %]</b>
> <br/>
> [% END %]
>
> <table><tr><td>
> <b>Nome</b>
> </td><td>
> <b>Telefone</td>
> </td><td>
> <b>Ação
> </td></tr>
>
> [% WHILE (item = items.next) %]
> <tr><td>
> [% item.nome %]
> </td><td>
> [% item.telefone %]
> </td><td>
> <a href="[% c.uri_for('edit', item.id
> <http://item.id>) %]">Editar</a>
> -
> <a href="[% c.uri_for('destroy', item.id
> <http://item.id>) %]">Remover</a>
> </td></tr>
> [% END %]
>
> </table>
>
> =head1 AUTOR
>
> Thiago Rondon <thiago em aware.com.br
> <mailto:thiago em aware.com.br>>
>
> =head1 COLABORADORES
> _______________________________________________
> SaoPaulo-pm mailing list
> SaoPaulo-pm em pm.org <mailto:SaoPaulo-pm em pm.org>
> http://mail.pm.org/mailman/listinfo/saopaulo-pm
>
>
>
> --
> http://mantovanihouse.blogspot.com/
>
> -------(\_------------_/)-----------
> -------)--(----------)--(-----------
> ------(----(---------)----)----------
> -------)----(-------)----(-----------
> -------(----(-------)----)-----------
> --------\_-(\\.---.//)-_/------------
> ----------\)' -8--8- '(/--------------
> -----------/------------\---------------
> ----------(--)--------(--)--------------
> ------------(_c__c_)----------------
> ----------------------------------------
>
> _______________________________________________
> SaoPaulo-pm mailing list
> SaoPaulo-pm em pm.org <mailto:SaoPaulo-pm em pm.org>
> http://mail.pm.org/mailman/listinfo/saopaulo-pm
>
>
>
> _______________________________________________
> SaoPaulo-pm mailing list
> SaoPaulo-pm em pm.org <mailto:SaoPaulo-pm em pm.org>
> http://mail.pm.org/mailman/listinfo/saopaulo-pm
>
>
>
>
>
> _______________________________________________
> SaoPaulo-pm mailing list
> SaoPaulo-pm em pm.org <mailto:SaoPaulo-pm em pm.org>
> http://mail.pm.org/mailman/listinfo/saopaulo-pm
>
>
>
>
> --
> --Lindolfo "Lorn" Rodrigues
> www.slackwarezine.com.br <http://www.slackwarezine.com.br>
> http://lornlab.org
> http://sao-paulo.pm.org
> use Catalyst;
> ------------------------------------------------------------------------
>
> _______________________________________________
> SaoPaulo-pm mailing list
> SaoPaulo-pm em pm.org
> http://mail.pm.org/mailman/listinfo/saopaulo-pm
More information about the SaoPaulo-pm
mailing list