CRUD com ExtJS, Python e MongoDB – parte 1

Filed Under (Desenvolvimento) by Samir on 30-06-2010

Tagged Under : , ,

Nesse exemplo dividido em algumas partes, vou exemplificar como criar aplicações com a ExtJs utilizando algumas técnicas avançadas que irá ajudar a tornar seu código organizado e reaproveitável. Na primeira parte iremos configurar o projeto utilizando o framework Cyclone e iniciar a primeira parte do cadastro. Porém antes é melhor explicar algumas coisas.

Pré-requisitos:

Nesse exemplo vamos utilizar:

O que é esse tal de Cyclone?

Bom o Cyclone é um servidor web non-blocking escrito em Python com suporte a HTTP 1.1 e toda sua API é inspirada no Tornado no qual se trata de outro servidor web extremamente rápido, escalável e open source.

A vantagem em utilizar um servidor como o Cyclone é que podemos colocar o Nginx na frente para servir arquivos estáticos e criar múltiplas instancias do Cyclone utilizando load balancer, isso dispensa a velha combinação de Nginx+Apache com o módulo WSGI instalado.

Outra razão para utilizar o Cyclone é sua integração com o driver assíncrono TxMongo, ambos são baseados no Twisted, por tanto para facilitar a integração, não vamos reiventar a roda.

Iniciando o projeto

Bom primeiramente vamos criar a estrutura de diretórios para nosso projeto, conforme a imagem abaixo, segue a estrutura do projeto (ignore o arquivo twistd.pid).

Front-End

No arquivo index.html, incluimos os arquivos necessários para o funcionamento correto da lib ExtJs, atenção para o método static_url que iremos configurar esse atributo a seguir. Observe também que ao carrega a página no método Ext.onReady, foi incluído alguns overrides para sobreescrever as configurações no ExtJs, dessa forma evitamos a duplicação de código e centralizamos em um único lugar.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pt-br">
    <head>
        <title>Exemplo Cadastro: ExtJs e Python</title>
        <meta http-equiv="Content-type"  content="text/html; charset=utf-8" />
        <link rel="stylesheet" href="{{ static_url("ext/resources/css/ext-all.css") }}" type="text/css"/>
        <script type="text/javascript" src="{{ static_url("ext/adapter/ext/ext-base.js") }}"></script>
        <script type="text/javascript" src="{{ static_url("ext/ext-all.js") }}"></script>
        <script type="text/javascript" src="{{ static_url("ext/locale/ext-lang-pt_BR.js") }}"></script>
    </head>
    <body>
        <div id="panel" style="padding:10px;"></div>
        <script type="text/javascript" src="{{ static_url("src/cadastro.js") }}"></script>
        <script type="text/javascript">
            Ext.onReady(function(){
                Ext.BLANK_IMAGE_URL = '{{ static_url("ext/resources/images/default/s.gif") }}';
                // overrides
                Ext.form.FormPanel.prototype.labelAlign = 'top';
                Ext.form.FormPanel.prototype.bodyStyle = 'padding:5px';
                Ext.layout.FormLayout.prototype.trackLabels = true;
                // qtips
                Ext.QuickTips.init();

                var panel = new App.Form({ renderTo:'panel' });
            });
        </script>
    </body>
</html>

Agora vamos criar o formulário via javascript no arquivo cadastro.js. Aqui criamos o namespace e a classe para montar o componente que será aplicado ao div “panel”.

Ext.namespace("App");

// Stores para Combos e Grids
App.Stores = {
    _storeTipoPessoa: new Ext.data.ArrayStore({
        data:[['Pessoa Física'],['Pessoa Jurídica']],
        fields: ['nome']
    })
};

// Formulario para Cadastro de Pessoas
App.Form = Ext.extend(Ext.form.FormPanel, {
    title:'Cadastro de Pessoas',
    width:550,
    frame:true,

    initComponent:function(){

        // combos
        var _comboTipoPessoa = new Ext.form.ComboBox({
            fieldLabel: 'Tipo Pessoa',
            hiddenName: 'tipo_pessoa',
            triggerAction: 'all',
            displayField: 'nome',
            allowBlank: false,
            editable: false,
            mode: 'local',
            store: App.Stores._storeTipoPessoa,
            width:115
        });

        // colunas
        var _coluna = [{
            width:120,
            items:[_comboTipoPessoa]
        },{
            width:205,
            items:[{
                fieldLabel:'Nome', name:'nome', allowBlank:false, width:200
            }]
        },{
            items:[{
                fieldLabel:'E-mail', name:'email', allowBlank:false, width:200, vtype:'email'
            }]
        },{
            items:[{
                xtype:'textarea', fieldLabel:'Observações', name:'observacoes', width:525, height:50
            }]
        }];

        // Aplicar os elementos ao formulário e configurar os botões para ações no servidor
        Ext.apply(this,{
            items:[{
                items:[{
                    layout:'column',
                    defaults:{
                        defaultType:'textfield', /* definir o componente padrão */
                        layout:'form'
                    },
                    items:[_coluna]
                }]
            }],
            buttons:[{
                text:'Salvar'
            },{
                text:'Novo'
            },{
                text:'Excluir'
            }]
        });

        // super
        App.Form.superclass.initComponent.call(this);
    }
});

Criamos o namespace “App” e em seguida o namespace “App.Stores”, acho interessante centralizar todos os stores em um único lugar pois facilita a manutenção conforme seu projeto cresce, por tanto qualquer Store local ou remoto deve ficar dentro desse namespace. Fica uma dica se achar necessário, colocar todos os stores em outro aquivo como stores.js.

Em seguida criamos o componente “App.Form” que herda as configurações de um FormPanel, porém, poderiamos muito bem “programar” no estilo abaixo.

Ext.onReady(function(){
   var form = new Ext.form.FormPanel({title:'Cadastro...'});
   form.render(document.body);
   ...
});

Esse modelo por mais que funcione sem problemas, na maioria das vezes é a pior forma de se trabalhar com a ExtJs, pois você acaba deixando sua programação Javascript totalmente estruturada e em se tratando de um aplicativo Ajax onde na maioria dos casos o aplicativo é carregado somente em uma página, fica praticamente inviável colocar toda a lógica no arquivo index ou mesmo em arquivos separados, por tanto sempre programe Javascript Orientado a Objetos, faz bem para saúde. :-D

Back-End

O backend sempre acaba sendo um caso a parte, a ExtJs não exige que você trabalhe com uma linguagem específica de servidor, por tanto qualquer linguagem que manipule o protocolo HTTP é válida, é muito comum encontrar exemplos de integração com PHP, C#, Java, Ruby, etc…mas aqui vamos usar Python pois é o foco do assunto.

Nessa primeira etapa vamos editar o arquivo server.tac e colocar a classe WebMongo para inicializar o servidor e também a classe IndexHandler para renderizar o template index.html, veja como é simples.

# coding:utf-8

import os.path
import txmongo
import cyclone.web
from twisted.internet import defer
from twisted.application import service, internet

class IndexHandler(cyclone.web.RequestHandler):
    def get(self):
        self.render("index.html")

class WebMongo(cyclone.web.Application):
    def __init__(self):
        handlers = [
            (r"/", IndexHandler)
        ]

        mongo = txmongo.lazyMongoConnectionPool()

        settings = dict(
            static_path="./static",
            template_path="./static",
            xsrf_cookies=True,
            mongo=txmongo.lazyMongoConnectionPool()
        )

        cyclone.web.Application.__init__(self, handlers, **settings)

application = service.Application("webmongo")
wmservice = internet.TCPServer(8888, WebMongo(), interface="127.0.0.1")
wmservice.setServiceParent(application)

Na classe IndexHandler que herda de RequestHandler utilizamos o método herdado “get” para renderizar o template index.html. A classe WebMongo é quem faz o trabalho de configurar as rotas da aplicação no objeto list “handler”, criamos uma instância para o MongoDB na variável “mongo” e logo em seguida criamos um dicionário com as configurações para arquivos estáticos e outras opções.

Observe que para arquivos estáticos o mais indicado seria colocar o Nginx para essa função, assim evitamos o overhead desnecessário no Cyclone deixando o servidor processar apenas código Python.

Colocando para funcionar…

Com o MongoDB previamente instalado e configurado, vamos inicializar o servidor do banco de dados e logo em seguida inicializar o servidor Cyclone via Twisted.

MongoDB:

$ /data/mongo/bin/./mongod

O resultado deve ser algo parecido com:

/data/mongo/bin/./mongod --help for help and startup options
Wed Jun 30 22:24:40 Mongo DB : starting : pid = 6138 port = 27017 dbpath = /data/db/ master = 0 slave = 0  32-bit 

** NOTE: when using MongoDB 32 bit, you are limited to about 2 gigabytes of data
**       see http://blog.mongodb.org/post/137788967/32-bit-limitations for more

Wed Jun 30 22:24:40 db version v1.4.2, pdfile version 4.5
Wed Jun 30 22:24:40 git version: 53749fc2d547a3139fcf169d84d58442778ea4b0
Wed Jun 30 22:24:40 sys info: Darwin broadway.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:55:01 PDT 2009; root:xnu-1228.15.4~1/RELEASE_I386 i386 BOOST_LIB_VERSION=1_40
Wed Jun 30 22:24:40 waiting for connections on port 27017
Wed Jun 30 22:24:40 web admin interface listening on port 28017

Twisted Application:

 $ twistd -ny server.tac

Resultado:

2010-06-30 22:30:19-0300 [-] Log opened.
2010-06-30 22:30:19-0300 [-] twistd 10.0.0 (/opt/local/Library/Frameworks/Python.framework/Versions/2.6/Resources/Python.app/Contents/MacOS/Python 2.6.4) starting up.
2010-06-30 22:30:19-0300 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2010-06-30 22:30:19-0300 [-] __builtin__.WebMongo starting on 8888
2010-06-30 22:30:19-0300 [-] Starting factory <__builtin__.WebMongo instance at 0x1779ee0>

No navegador, acessar pelo endereço http://localhost:8888, o resultado final será parecido com a imagem abaixo.

Para visualizar os fontes da primeira parte, faça o download aqui. Na próxima parte vamos começar a integração de fato com MongoDB e estudar como aplicar o paradigma MVC de uma forma um pouco diferente mas totalmente funcional e muito produtiva.