Skip to content

CRUD com ExtJS, Python e MongoDB – parte 1

30-Jun-10

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.

Python e MongoDB

24-May-10

Parece que agora a nova tendência é mais e mais pessoas começarem a adotar bancos de dados NoSql e o MongoDB vem se destacando cada vez mais pela simplicidade, estabilidade e muito mas muito rápido!

Tem uma apresentação muito boa da PyCon 2010 onde Rick Copeland que trabalha na SourceForge, relata como migraram o portal utilizando Python, TurboGears e MongoDB, veja aqui.

Bom, como eu sempre destetei SQL a vida toda e acho que nunca vou aprender a gostar, comecei a estudar a possibilidade de migrar o MongoDB no sistema que venho trabalhando (segredo de estado!)…afinal minha vontade sempre foi ter algo mais voltado para Orientação Objetos e como eu sempre tive que recorrer a ORM’s da vida…, era sempre aquela duplicação de código interminável, mas enfim, depois de trabalhar por 2 semanas quase 15 horas por dia consegui migrar tudo e pra minha sorte, consegui reduzir drasticamamente o código do projeto. Com base nessa pequena experiência que passei, vou relatar as possibilidades que testei em Python e não foram muitas.

PyMongo:

O PyMongo é o driver oficial desenvolvido pela equipe do MongoDB, achei um pouco confuso no início, porque na verdade não se cria Models e nenhum tipo de Mapeamento de Objeto Relacional. Você simplesmente se conecta no banco, invoca a collection relacionada (ou tabela para melhor entendimento), passa um dicionário de dados em Python e manda salvar.

Exemplo:

import datetime
from pymongo import Connection

connection = Connection()
db = connection.test_database

post = {"author": "Samir", "text": "Python e MongoDB", "tags": ["mongodb", "python", "pymongo"], "date":datetime.datetime.utcnow()}
db.posts.save(post)

Esse modelo pode funcionar muito bem se você quebrar o código em partes, ou seja, um módulo ou classe que gerencia a conexão com banco de dados e se preferir pode-se criar classes para se representar uma Collection como no exemplo, criar uma classe Post, isso pode ser uma boa prática, mas não caia na besteira de mapear a classe com as colunas de uma Collection, você vai duplicar seu código atoa e só complicar as coisas, pois o MongoDB não segue a filosofia do modelo Relacional.

Depois de alguns testes e pesquisando mais opções eu decici não trabalhar com o driver diretamente, a única razão foi porque o PyMongo trabalha de forma síncrona e no meu projeto tudo é assíncrono e vou relatar isso logo abaixo.

MongoEngine

O MongoEngine foi outra opção que testei, ele é uma espécie de ORM que executa sobre o PyMongo, porém é um projeto muito recente e com pouca atividade, não aconselho a usar ainda.

A ideia do MongoEngine é trabalhar exatamente como o ORM do Django, você mapeia uma classe e usa os métodos para consultar e salvar objetos. Até ai tudo bem, mas de novo cai no velho problema, não tem o menor sentido usar ORM com o MongoDB, é claro que para fins de documentação esse pode ser o modelo ideal.

Exemplo:

from mongoengine import *
connect('test_database')

# Models
class User(Document):
    email = StringField(required=True)
    first_name = StringField(max_length=50)
    last_name = StringField(max_length=50)

class Post(Document):
    title = StringField(max_length=120, required=True)
    content = StringField(max_length=255, required=True)
    author = ReferenceField(User)
    tags = ListField(StringField(max_length=30))

# Salvando objetos
john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
john.save()

post = Post(title='Fun with MongoEngine', author=john)
post.content = 'Content here...'
post.tags = ['mongodb', 'mongoengine']
post.save()

# Consultando...
for post in Post.objects:
    print post.title

TxMongo

O TxMongo foi a melhor opção que encontrei, ele também roda sobre o PyMongo, porém toda sua estrutura é baseada no Twisted. A maior vantagem que percebi é por ele ser assíncrono e ter a possibilidade de criar vários pools de conexões ao mesmo tempo. O PyMongo por padrão cria apenas um pool de conexão o que limita bastante a performance da aplicação para múltiplos requests.

O modelo de implementação é praticamente identico ao PyMongo porém como ele é baseado no Twisted, antes precisa enteder como essa biblioteca funciona, confesso que apanhei um monte para aprender, mas vale a pena! :-D

Exemplo:

import time
import txmongo
from twisted.internet import defer, reactor

@defer.inlineCallbacks
def example():
    mongo = yield txmongo.MongoConnection()

    foo = mongo.foo # `foo` database
    test = foo.test # `test` collection

    # insert some data
    for x in xrange(10000):
        result = yield test.insert({"something":x*time.time()}, safe=True)
        print result

if __name__ == '__main__':
    example().addCallback(lambda ign: reactor.stop())
    reactor.run()

Conclusão:

Bom depois de testar essas possibilidades, o projeto vem sendo desenvolvido com ExtJS para o tudo o que imaginar no FrontEnd e no BackEnd substitui o Django pelo Cyclone que é um mini framework web assíncrono também baseado no Twisted que se integra perfeitamente com o TxMongo. No próximo post, pretendo exemplificar como integrar essas bibliotecas. Críticas ou sugestões serão bem vindas ;)

Django – Separando Models por arquivo

19-Mar-10

Uma dúvida muito comum sobre o ORM do Django é se existe a possibilidade de quebrar os models em partes menores, ou seja, separar um grupo de classes específicas e coloca-lás em um módulo Python.

Essa técnica pode ser útil conforme sua necessidade, se você possui uma app com muitos models, por ser interessante separa-lás por arquivo. Eu tive esse problema recentemente, procurei na net e achei uma forma interessante de fazer isso e principalmente, não quebrar a compatibilidade com as ferramentas externas, como o syncdb ou o south que utilizo bastante.

Sem muita enrolação, imagine a seguinte estrutura para um projeto:

Transferências

Tendo a estrutura pronta, temos que editar os models de forma que o Django entenda nossa nova estrutura.
Por exemplo no arquivo financeiro.py da app2:

from django.db import models

class Financeiro(models.Model):
    class Meta:
        app_label = 'app2'

Observe que sou obrigado a informar explicitamente o nome da aplicação para cada model, sem isso o Django não consegue entender que essa classe pertence a aplicação app2.

E por último, no arquivos models.py de cada aplicação temos que importar as classes:

from app2.models_src.financeiro import Financeiro

Uma coisa muito importante que passei entender sobre o Django é que ele lhe dá muita flexibilidade para modificar sua estrutura e sobreescrever qualquer feature padrão, por tanto quanto melhor seu conhecimento em Python, melhores serão suas chances de extrair o máximo do framework.

Integração entre Django e ExtJs

25-Feb-10

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