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.

Python e MongoDB

Filed Under (Desenvolvimento) by Samir on 24-05-2010

Tagged Under : ,

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 ;)

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

Desenvolvendo Sistemas com Django

Filed Under (Desenvolvimento) by Samir on 03-12-2009

Tagged Under : ,

Essa é uma questão comum que ja vi lista #django-brasil onde muitos desenvolvedores ficam na dúvida para escolher a melhor opção de criar sistemas usando o Django. Fazer tudo no “braço” mesmo criando suas próprias views, htmls, rotas, etc…tudo para ter a melhor flexibilidade possível. Ou como segunda opção usar o próprio admin e aprender como customizar quando necessário.

Antes de iniciar o projeto que ando trabalhando, testei muitas opções de frameworks. Queria algo que pudesse agilizar ao máximo meu trabalho, porque convenhamos, ficar fazendo tela de CRUD alem de ser um trabalho maçante, propenso a erros, é um verdadeiro pé no saco! Nessa época como ainda estava ligado muito no Rails, tentei algumas opções, mas tudo me levava a fazer o CRUD na mão. Voltei novamente a pesquisar opções em Python, nessa época eu já até conhecia bem o admin do Django, mas tinha um certo receio do tipo, “esse negócio vai me deixar na mão”, mas foi puro engano, vou explicar adiante.

Continuando minha pesquisa, conheci o framework “cola” Pylons, na primeira vez fiquei impressionado, extremamente versátil e muito parecido com Rails, mas após alguns testes percebi que não tinha experiência o suficiente para usar essa ferramenta, acho que esse framework é para quem tem um conhecimento avançado em Python e não é meu caso.

Também testei o web2py, outro framework interessante e ágil para projetos pequenos, mas acho que a forma de trabalhar nele é muito estranha, você edita o código no próprio navegador como se fosse seu próprio IDE e assim por diante. Mas o que realmente me incomodou é que ele segue uma linha contrária do Python onde não existe importação de módulos como é de costume. E como tudo no Python é explícito e sem “mágicas”, deixei essa opção de lado e acabei voltando para o Django novamente usando o Admin.

A primeira coisa que recomendo antes de se aventurar a criar um sistema usando o Admin é, saiba bem como usar a Orientação a Objetos do Python e porque isso? No início tive a sensação do Admin ser aquele código “engessado” onde você fica amarrado e sem ter muito o que fazer. Mas com o tempo fui percebendo que você pode sobreescrever qualquer método da biblioteca padrão e customizar da maneira de achar necessário, vamos a um pequeno exemplo.

O sistema que estou criando, cada cliente possui seu sub-dominio e essa informação não pode ser editada como é por padrão, então imagine o seguinte trecho de código.

# models.py
class Cliente(models.Models):
    dominio = models.CharField(max_length=50)
    nome_fantasia = models.CharField(max_length=100)
    plano_acesso = models.ForeignKey('PlanoAcesso')

# forms.py
class ClienteAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ClienteAdminForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
            self.fields['dominio'].widget.attrs['readonly'] = True
            self.fields['dominio'].help_text = 'Acesso: http://%s.dominio.com.br:8090' % instance.dominio

# admin.py
class ClienteAdmin(admin.ModelAdmin):
    form = ClienteAdminForm
    fields = ('dominio','nome_fantasia',...,)
register.admin.site(Cliente, ClienteAdmin)

Observando bem o código, o que fiz ali foi sobreescrever o formulário padrão da classe ClienteAdmin pela classe ClienteAdminForm e no construtor dessa classe eu verifico se o objeto está no modo de edição e travo o campo domínio como “readonly”. Isso é apenas uma pequena amostra do que é possível fazer, o Django é flexível o suficiente de acordo com sua necessidade.

Outra coisa muito interessante é que podemos trabalhar com herança nos templates sem ter a necessidade de modificar o arquivo original do admin, basta criar um novo arquivo com o mesmo nome e herdar do padrão, por exemplo. Na minha lista de clientes, eu quero somar o total do valor dos planos de acessos.

Primeiro mudo o comportamento padrão do método changelist_view:

# admin.py
def changelist_view(self, request, extra_context={}):
   soma = PlanoAcesso.objects.all().aggregate(Sum('valor'))
   try:
      extra_context['soma'] = ("R$ %2.2f" % soma['valor__sum']).replace('.',',')
   except:
      extra_context['soma'] = "R$ 0,00"
   return super(ClienteAdmin, self).changelist_view(request, extra_context)

Para criar o template vamos supor que essa aplicação seja chamada de clientes, dentro dessa pasta crie outra pasta chamada templates e dentro dela outra chamada admin, novamente dentro da pasta admin crie a pasta clientes e dentro de clientes cria a última pasta cliente, achou confuso? :D

Nessa última pasta, crie o arquivo change_list.html com o seguinte conteúdo.

{% extends "admin/change_list.html" %}
{% block result_list %}
    {{ block.super }}
    Total: {{ soma }}
{% endblock result_list %}

Se você observar, temos a listagem original porque no html chamamos a variável {{ block.super }} para herdar do template original e logo abaixo nosso conteúdo customizável, muito legal não?

Para finalizar, acho que ficou claro que podemos usar sim o Admin para criação de sistemas complexos e customizá-lo ao máximo, basta saber usar a ferramenta corretamente e sem gambiarras.

ads
ads
ads
ads