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.

Integração entre Django e ExtJs

Filed Under (Desenvolvimento) by Samir on 25-02-2010

Tagged Under : ,

Recentemente voltei minhas atenções para a biblioteca javascript ExtJs. O projeto/sistema que venho trabalhando desde outubro de 2009 passou por várias mudanças e uma coisa que eu estava achando ruim era justamente a user interface, afinal não sou um designer :P

Conversando com o cliente mostrei a biblioteca, expliquei suas vantagens e tive aprovação para refazer a interface. A primeira dúvida era como integrar a chamadas ajax com o Django, já que o mesmo não tem suporte nativo e como serializar objetos de forma flexível. O Django possui um sistema de serialização para os formatos mais conhecidos, mas a  forma como os objetos são serializados não me agrada, sendo assim comecei a procurar por alternativas.

Existem poucas alternativas de integração entre Python/ExtJs, das alternativas que  testei foram o ExtDirect-Django e Piston.

No final das contas acabei criando uma classe bem simples para serializar objetos de acordo com a minha necessidade, até agora vem funcionando bem e quem quiser melhorar o código fique a vontade :)

Arquivo json.py

# -*- coding:utf-8 -*-

from django.http import HttpResponse
from django.core import serializers
from django.utils import simplejson
from django.utils.encoding import smart_unicode

# ********************************************
def ext_date(d, date_format=1):
    """
    Renderizar Datetime no formato para ExtJs
    """
    if date_format==1:
        return d.strftime("%m/%d/%Y")
    elif date_format==2:
        return d.strftime("%d/%m/%Y")

# ********************************************
class JsonResponse(HttpResponse):
    """
    Renderizar Dicionrario Python para formato JSON
    """
    def __init__(self, params={}):
        HttpResponse.__init__(self, content=simplejson.dumps(params), mimetype='application/json')

# ********************************************
class JsonModelResponse(HttpResponse):
    """
    Renderizar Model para formato JSON
    """
    def __init__(self, queryset, **options):
        excludes = options.get('excludes')
        date_format = options.get('date_format')
        content = {'success': True}
        fields, many_to_many, foreignkeys = {},{},{}

        opts = queryset._meta # Extrai informação do Model

        # colunas
        for c in opts.fields:
            if excludes is not None:
                if c.name in excludes:
                    continue

            if c.__class__.__name__ == 'DateTimeField':
                if date_format is not None:
                    fields.update({c.name: ext_date(getattr(queryset, c.attname), date_format=date_format)})
                else:
                    fields.update({c.name: ext_date(getattr(queryset, c.attname))})
            elif c.__class__.__name__ == 'DecimalField':
                fields.update({c.name: str(getattr(queryset, c.attname))})
            elif c.__class__.__name__ == 'ForeignKey':
                pk = getattr(queryset, c.attname)
                # Usa o metodo smart_unicode para serializer um objeto model
                val = smart_unicode(getattr(queryset, c.name), strings_only=True)
                foreignkeys.update({c.name: [pk,val]})
            else:
                fields.update({c.name: getattr(queryset, c.attname)})

        # relaciomentos
        for c in opts.many_to_many:
            models = c.value_from_object(queryset) # Extrair models da lista
            many_to_many.update({c.name: [[{m.name:getattr(model, m.name)} for m in model._meta.fields] for model in models]})

        # atualiza dict
        content.update({'data': fields, 'foreignkeys': foreignkeys, 'many_to_many': many_to_many})
        # retorna a reposta no formato json
        HttpResponse.__init__(self, content=simplejson.dumps(content), mimetype='application/json')

# ********************************************
class JsonSuccess(HttpResponse):
    """
    Resposta JSON para requests efetuados com sucesso
    """
    def __init__(self, params={}):
        content = {'success':True}
        content.update(params)
        HttpResponse.__init__(self, content=simplejson.dumps(content), mimetype='application/json')

# ********************************************
class JsonFailure(HttpResponse):
    """
    Resposta JSON para requests efetuados com falha
    """
    def __init__(self, params={}):
        content = {'success':False}
        content.update(params)
        HttpResponse.__init__(self, content=simplejson.dumps(content), mimetype='application/json')

A integração fica bem mais simples agora, veja um pequeno exemplo de uma chamada Ajax.

Ext.onReady({
   Ext.Ajax.request({
      method:'GET',
      url:'/usuarios/show/1/'
   });
});

No lado do servidor nossa view faz a utilização do módulo json que criamos acima e devolve para o client no formato Json. Observe que na resposta não quero se seja serializado o campo password na resposta.

from utils import json
from django.contrib.auth.models import User

def show(request,id):
     qs = User.objects.get(pk=id)
     return json.JsonModelResponse(qs, excludes=('password',))

Observando a resposta do servidor pelo Firebug, temos o seguinte:
json

Interessante não? :D

ads
ads
ads
ads