Django class based views com lazy loading

Vejamos um pequeno exemplo de uso do class based views no Django:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher
# urls.py
from django.conf.urls import patterns, url
from books.views import PublisherList

urlpatterns = patterns('',
    url(r'^publishers/$', PublisherList.as_view()),
)

O problema desse código é que você precisa importar o PublisherList no urls.py, para pequenos projetos isso não é problema, mas se o projeto for muito grande o tempo de carregamento do urls.py será comprometido.

Usando o paradigma tradicional de views (uma função que recebe request e retorna um HttpResponse) podemos carregar essa view apenas quando necessário, isso é chamado de lazy loading, muito melhor para grandes projetos, pois a importação de todas as views no urls.py é desnecessária.

Podemos resolver isso de uma maneira muito fácil:

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher

publisher_list = PublisherList.as_view()
# urls.py
from django.conf.urls import patterns, url

urlpatterns = patterns('',
    url(r'^publishers/$', 'myapp.views.publisher_list'),
)

Buscando cep diretamente pelo site dos correios em python

Todos que trabalham com desenvolvimento de sistemas já se depararam com o seguinte problema: Como faço para buscar um endereço pelo cep ou vice-versa?

Existe um banco de ceps que pode ser encontrado facilmente usando algum sistema de buscas pela web, o problema é que esse banco é de 2009 e provavelmente desatualizado. Para vergonha dos brasileiros, o site dos correios não provê um webservice oficial para busca de ceps, o máximo que oferece é um form em que os resultados são devolvidos em html mesmo, nada de retorno em json/xml, inclusive os correios te vendem uma base de cep com 900 mil ceps pela pechincha de R$ 1.100,00.

Uma solução viável (até que os correios não botem um captcha na busca) é fazer um screen-scraping em python do resultado html devolvido, para isso já existe uma ótima biblioteca chamada cep que resolve esse problema.

Então eu tive a seguinte ideia, porque não usar o tornado para servir como um webservice do resultado que a biblioteca cep devolve? Eu posso usar o async http cliente do tornado e assim conseguir retornar esses resultados sem bloquear outras requisições que chegam ao meu servidor.

Fazendo um projetinho de 15 minutos eu cheguei no seguinte código (claro, ainda dá pra refatorar muita coisa ainda):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.httpclient
import tornado.escape

from urllib import urlencode

# http://pypi.python.org/pypi/cep
from cep import Correios

class MainHandler(tornado.web.RequestHandler):
    
    def get(self):
        self.write(
            '<html><body><form action="/search" method="get">'
            '<input type="text" name="query">'
            '<input type="submit" value="Pesquisar">'
            '</form></body></html>'
        )

class SearchHandler(tornado.web.RequestHandler):
    
    @tornado.web.asynchronous
    def get(self):
        # get search argument
        query = self.get_argument('query', None)
        # write error for empty argument
        if not query or query == '':
            self.write("Por favor preencha o formulário de busca")
            self.finish()
        # create data for request body
        data = dict(
            EndRow='10', 
            Metodo='listaLogradouro', 
            StartRow='1', 
            TipoConsulta='relaxation',
            relaxation=query.encode('cp1252'),
        )
        # create http request
        request = tornado.httpclient.HTTPRequest(
            'http://www.buscacep.correios.com.br/servicos/dnec/consultaLogradouroAction.do',
            method='POST',
            body=urlencode(data, True)
        )
        # create async http client
        http = tornado.httpclient.AsyncHTTPClient()
        # load page
        http.fetch(request, callback=self.process_response)
        
    def process_response(self, response):
        # if error raise status code 500
        if response.error: 
            raise tornado.web.HTTPError(500)
        # cep parsing
        c = Correios()
        resultados = c._parse_tabela(response.body)
        # create json
        json = tornado.escape.json_encode(resultados)
        # output json
        self.set_header('Content-Type', 'application/json')
        self.write(json)
        self.finish()

application = tornado.web.Application([
    tornado.web.url(r'/', MainHandler, name='main'),
    tornado.web.url(r'/search', SearchHandler, name='search'),
])

if __name__ == '__main__':
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Basicamente temos apenas dois handlers, o primeiro é um form com o campo query e o segundo é um handler assíncrono que recebe a query e processa uma request para o site dos correios, ao receber o html eu passo o processamento para a biblioteca cep e finalmente devolvo o resultado em json.

Pronto, agora você pode rodar um webservice local para buscar os ceps diretamente dos correios, lógico que esse projeto pode melhorar bastante, podemos guardar os resultados em um banco de dados evitando que sempre seja feita uma requisição ao site dos correios.

Gerando slugs usando o Django Signals

É muito comum o uso de slugs na construção de urls elegantes, o Django já vem com uma função chamada slugify que transforma um string no formato de slug.

Utilizando o signals, podemos criar uma função que faz o processamento do slug após o registro ser criado no banco, ficando transparente para o usuário final.

Eu vou criar um model chamado category com name e slug

# models.py
# -*- coding: utf-8 -*-
from django.db import models

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

Note que é interessante usar mais caracteres no max_length do slug, aqui temos o name com max_length=100 e o slug com max_length=150.

Agora para um melhor controle, vamos adicionar outros dois atributos:
slug_field_name: informa qual é o atributo slug
slug_from: informa o atributo de onde o slug vai ser gerado

# models.py
# -*- coding: utf-8 -*-
from django.db import models

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)
    # Field to slug
    slug_field_name = 'slug'
    slug_from = 'name'

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

Agora vamos escrever a função que vai tratar o signal

# signals.py
# -*- coding: utf-8 -*-
from django.template.defaultfilters import slugify

def create_slug(sender, instance, signal, *args, **kwargs):
    # check for id and attributes
    if instance.id and hasattr(instance, 'slug_field_name') and hasattr(instance, 'slug_from'):
        # get slug information
        slug_name = instance.slug_field_name
        slug_from = instance.slug_from
        # save slug if empty
        if not getattr(instance, slug_name, None):
            # create slug
            slug = '%s-' % instance.id + slugify(getattr(instance, slug_from))
            # set slug
            setattr(instance, slug_name, slug)
            # save instance
            instance.save()

A função create_slug checa se a instância em questão possui id, slug_field_name e slug_from, caso o slug não esteja preenchido um novo slug é gerado e gravado.

Para geração de slugs eu utilizei a concatenação do id do objeto e o slug, por isso que é sempre interessante deixar o max_length do slug maior do que a origem dele. Dessa forma nunca vai existir slugs em duplicidade, porque o id da instância sempre é diferente.

Para finalizar, basta adicionar o signal no models.py

# models.py
# -*- coding: utf-8 -*-
from django.db import models
from django.db.models import signals

from signals import create_slug

class Category(models.Model):
    # category name
    name = models.CharField(u'nome', max_length=100)
    # category slug
    slug = models.SlugField(max_length=150, editable=False)
    # Field to slug
    slug_field_name = 'slug'
    slug_from = 'name'

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u'categoria'
        verbose_name_plural = u'categorias'

# Signals
signals.post_save.connect(create_slug, sender=Category)

Como essa função é genérica, você pode usar ela em qualquer model, basta informar o slug_field_name e slug_from.

Algumas dicas sobre o Django Forms

As dicas desse post foram tiradas da apresentação Advanced Django Forms Usage da DjangoCon, recomendo que todos vejam o vídeo e os slides.

Vídeo:

Slides: http://speakerdeck.com/u/maraujop/p/advanced-django-forms-usage

Esse primeiro exemplo mostra o uso tradicional de um form dentro de uma view do Django:

def my_view(request, template_name='myapp/my_form.html'):

    if request.method == 'POST':
        form = MyForm(request.POST) # Form #1!
        if form.is_valid(): # nested if!
            do_x()
            return redirect('/')
    else:
        form = MyForm() # Form #2!
        
    return render(request, template_name, {'form': form})

O principal problema desse código é que você tem o form declarado duas vezes quando é possível declarar uma única vez. Podemos reescrever esse código dessa forma:

def my_view(request, template_name='myapp/my_form.html'):

    # sticks in a POST or renders empty form
    form = MyForm(request.POST or None)
    if form.is_valid():
        do_x()
        return redirect('home')
    return render(request, template_name, {'form': form})

Agora temos menos linhas de código, o form declarado uma única vez e um if a menos.

No caso de um form com upload de arquivos, podemos fazer assim:

def my_view(request, template_name='myapp/my_form.html'):

    form = MyForm(request.POST or None, request.FILES or None)
    if form.is_valid():
        do_x()
        return redirect('home')
    return render(request, template_name, {'form': form})

Em todo projeto Django é comum que se use bastante os modelforms, nesse caso o uso tradicional é dessa forma:

def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'):

    # I wouldn't call the variable model, because it's an instance of a model, it's confusing
    mymodel = get_object_or_404(MyModel, slug=slug)
    if request.method == 'POST':
        form = MyForm(request, instance=mymodel)
        if form.is_valid():
            mymodel = form.save()
            mymodel.day_shown = datetime.datetime.now() # Do any extra model stuff here
            mymodel.save()
            return redirect('home')
    else:
        form = MyForm(instance=mymodel)
    return render(request, template_name, {'form': form, 'model': mymodel})

Aqui temos o mesmo problema do form simples, o form é declarado duas vezes, podemos reescrever dessa forma:

def my_model_edit(request, slug=slug, template_name='myapp/my_model_form.html'):

    mymodel = get_object_or_404(MyModel, slug=slug)
    
    form = MyModelForm(request.POST or None, instance=mymodel)
    
    if form.is_valid():
        mymodel = form.save()
        mymodel.edited_at_djangocon = True
        mymodel.save()
        return redirect('home')
    
    return render(request, template_name, {'form': form, 'mymodel': mymodel})

Novamente temos menos linhas de código, o form declarado uma única vez e apenas um if.

Para um modelform de adição o código ficaria o seguinte:

def my_model_add(request, template_name='myapp/my_model_form.html'):
    
    form = MyModelForm(request.POST or None)
    
    if form.is_valid():
        mymodel = form.save()
        mymodel.added_at_djangocon = True
        mymodel.save()
        return redirect('home')
    
    return render(request,template_name,{'form': form})

Além dessas dicas existem várias outras na apresentação, recomendo mais uma vez a todos que assistam e baixem os slides.

Introdução ao WSGI

WSGI (Web Server Gateway Interface) é uma especificação de interface que permite a comunicação entre o servidor e sua aplicação python, o PEP 333 descreve em detalhes toda especificação do WSGI 1.0 (existe o PEP 3333 que descreve o WSGI 1.0.1, mas o padrão utilizado atualmente ainda é o 1.0).

O Gunicorn é projeto de maior destaque quando se fala de servidor WSGI nativo, com ele não é preciso de nenhum adaptador WSGI para outro servidor web, apesar disso é bem comum que se utilize um servidor web para servir conteúdo estático e passar o conteúdo dinâmico para o servidor WSGI usando o esquema de proxy reverso.

O mod_wsgi e uwsgi são os maiores projetos em termos de adaptadores WSGI, o mod_wsgi roda em conjunto com o Apache, já o uwsgi roda em conjunto com NGINX.

O grande benefício do WSGI é que já existem servidores/adaptadores bastante eficientes, então você só precisa se preocupar com a parte da aplicação em si, além disso, praticamente todos os frameworks web fazem uso do WSGI o que torna ele o padrão para deploy de aplicações.

Um exemplo de hello world com wsgi

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

def hello_world_wsgi(environ, start_response):
    '''
    Uma função simples que demonstra como fazer um hello world com WSGI.
    
    Essa função deve receber dois argumentos environ e start_response.

    O environ contém um dicionário em python com valores preenchidos pelo
    servidor em cada requisição recebida. 
    Para saber mais sobre as variáveis que vem no environ visite: 

http://www.python.org/dev/peps/pep-0333/#environ-variables

    O start_response é um callback enviado pelo servidor para que sua aplicação 
    responda a requisição.

    '''
    # o status segue o padrão do w3:
    # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
    status = '200 OK'
    # response body é o conteúdo que deve ser retornado
    response_body = 'Hello World!'
    # response_headers é o cabeçalho de resposta, geralmente contém o tipo
    # de conteúdo retornado e o tamanho.
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    # a resposta deve ser iniciada utilizando o start_response
    start_response(status, response_headers)
    # por fim o conteúdo deve ser retornado utilizando alguma estrutura python
    # iterável, nesse caso uma lista foi usada.
    return [response_body]

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, hello_world_wsgi)
    srv.serve_forever()

Claro que esse exemplo pode ser melhorado, vejamos outro exemplo mais elaborado

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

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    Caso a requisição seja para a raíz do site '/' o index_handler vai ser
    carregado, caso contrário o not_found_handler é carregado.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path é a raíz do site
    if path == '/':
        return index_handler(environ, start_response)
    else:
        return not_found_handler(environ, start_response)

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application_handler)
    srv.serve_forever()

Podemos melhorar a maneira que as urls são chamadas

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def application_handler(environ, start_response, urls=urls):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '').lstrip('/')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    for regex, callback in urls:
        match = re.search(regex, path)
        if match is not None:
            return callback(environ, start_response)
    return not_found_handler(environ, start_response)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application_handler)
    srv.serve_forever()

O código acima tem um pequeno problema, ao acessar o /hello e /hello/ você recebe resultados diferentes, o primeiro retorna 404 enquanto o segundo retorna a página do hello world. Isso acontece por causa do ‘/’ no final da url, seria interessante escrever um middleware que adicionasse essa barra de forma automática se necessário.

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def fetch_url(url, urls=urls):
    '''
    Função para checar se uma url está listada na lista de urls.

    Retorna o callback em caso positivo e None em caso negativo.
    '''
    url = url.lstrip('/')    
    for regex, callback in urls:
        match = re.search(regex, url)
        if match:
            return callback
    return None

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    callback = fetch_url(path)
    if callback:
        return callback(environ, start_response)
    return not_found_handler(environ, start_response)

class AddSlashMiddleware(object):
    '''
    Middleware que verifica se a url requisitada em combinação com o '/' no
    final é válida, em caso afirmativo deve ocorrer um redirecionamento.
    '''
    def __init__(self, application):
        self.application = application
        
    def __call__(self, environ, start_response):
        # Recebe o path
        path = environ.get('PATH_INFO', '')
        if not path.endswith('/'):
            # Se o path não termina com '/', verificar se esse path 
            # combinado com o '/' existe na lista de urls
            callback = fetch_url(path + '/')
            if callback:
                # Redireciona para url
                uri = path + '/'
                start_response('302 FOUND', [('Location', uri)])
                return []
        # Passa o controle para aplicação
        return self.application(environ, start_response)

# cria um application que passa pelo middleware
application = AddSlashMiddleware(application_handler)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application)
    srv.serve_forever()

Para finalizar, vamos escrever outro middleware, desta vez eu quero que o html retornado fique todo em letra minúscula.

# -*- coding: utf-8 -*-
import re

def index_handler(environ, start_response):
    '''
    Retorna a página index do site.
    '''
    status = '200 OK'
    response_body = '<h1>Welcome!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def hello_handler(environ, start_response):
    '''
    Retorna a página hello world do site.
    '''
    status = '200 OK'
    response_body = '<h1>Hello World!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

def not_found_handler(environ, start_response):
    '''
    Retorna a página 404 de um site.
    '''
    status = '404 NOT FOUND'
    response_body = '<h1>Not found!</h1>'
    response_headers = [
        ('Content-Type', 'text/html'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
    return [response_body]

# Lista de urls, cada elemento é uma tupla com dois valores: uma expressão
# regular da url e a função que vai processar a requisição, muito parecido com
# o esquema do django.
urls = [
    (r'^$', index_handler),
    (r'^hello/$', hello_handler),
]

def fetch_url(url, urls=urls):
    '''
    Função para checar se uma url está listada na lista de urls.

    Retorna o callback em caso positivo e None em caso negativo.
    '''
    url = url.lstrip('/')    
    for regex, callback in urls:
        match = re.search(regex, url)
        if match:
            return callback
    return None

def application_handler(environ, start_response):
    '''
    Função responsável por gerenciar as requisições WSGI.
    
    As requisições são despachadas de acordo com a url da lista de urls.
    '''
    # recebendo o valor do PATH
    path = environ.get('PATH_INFO', '')
    # verificando se o path tem algum handler correspondente, caso contrário
    # retorne 404
    callback = fetch_url(path)
    if callback:
        return callback(environ, start_response)
    return not_found_handler(environ, start_response)

class AddSlashMiddleware(object):
    '''
    Middleware que verifica se a url requisitada em combinação com o '/' no
    final é válida, em caso afirmativo deve ocorrer um redirecionamento.
    '''
    def __init__(self, application):
        self.application = application
        
    def __call__(self, environ, start_response):
        # Recebe o path
        path = environ.get('PATH_INFO', '')
        if not path.endswith('/'):
            # Se o path não termina com '/', verificar se esse path 
            # combinado com o '/' existe na lista de urls
            callback = fetch_url(path + '/')
            if callback:
                # Redireciona para url
                uri = path + '/'
                start_response('302 FOUND', [('Location', uri)])
                return []
        # Passa o controle para aplicação
        return self.application(environ, start_response)

class LowercaseMiddleware(object):
    '''
    Middleware que converte o html todo em letras minúsculas.
    '''
    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):
        # Faço uma iteração convertendo o valor obtido para minúsculas
        for chunk in self.application(environ, start_response):
            yield chunk.lower()

# Aplicando o  AddSlashMiddleware
application = AddSlashMiddleware(application_handler)
# Aplicando o LowercaseMiddleware
application = LowercaseMiddleware(application)

# iniciando um servidor wsgi na porta 8080
if __name__ == '__main__':
    from wsgiref.simple_server import make_server
    srv = make_server('localhost', 8080, application)
    srv.serve_forever()

Agora temos dois middlewares que são aplicados ao nosso aplicativo, lembrando que a ordem que os middlewares são aplicados é importante.

Como foi visto, o WSGI permite que se criem aplicações de forma bastante simples, recomendo a leitura do Learn WSGI que foi a referência para esse post.

Alterando a paginação padrão do Django sitemaps

O Django já conta com um aplicativo no contrib para gerar sitemaps, recentemente eu tive alguns problemas com o padrão de objetos por arquivo xml gerado que é de 50 mil por página. Como o sistema tinha certa de 120 mil objetos ficaria muito pesado essa consulta para o banco e também o tempo de renderização do template.

Investigando no código fonte do Django eu percebi como é montado o GenericSitemap :

# trechos do arquivo django/contrib/sitemaps/__init__.py
class Sitemap(object):
    # This limit is defined by Google. See the index documentation at
    # http://sitemaps.org/protocol.php#index.
    limit = 50000

    def __get(self, name, obj, default=None):
        try:
            attr = getattr(self, name)
        except AttributeError:
            return default
        if callable(attr):
            return attr(obj)
        return attr

    def items(self):
        return []

    def location(self, obj):
        return obj.get_absolute_url()

    def _get_paginator(self):
        if not hasattr(self, "_paginator"):
            self._paginator = paginator.Paginator(self.items(), self.limit)
        return self._paginator
    paginator = property(_get_paginator)

    def get_urls(self, page=1, site=None):
        if site is None:
            if Site._meta.installed:
                try:
                    site = Site.objects.get_current()
                except Site.DoesNotExist:
                    pass
            if site is None:
                raise ImproperlyConfigured("In order to use Sitemaps you must either use the sites framework or pass in a Site or RequestSite object in your view code.")

        urls = []
        for item in self.paginator.page(page).object_list:
            loc = "http://%s%s" % (site.domain, self.__get('location', item))
            priority = self.__get('priority', item, None)
            url_info = {
                'location':   loc,
                'lastmod':    self.__get('lastmod', item, None),
                'changefreq': self.__get('changefreq', item, None),
                'priority':   str(priority is not None and priority or '')
            }
            urls.append(url_info)
        return urls

class FlatPageSitemap(Sitemap):
    def items(self):
        current_site = Site.objects.get_current()
        return current_site.flatpage_set.filter(registration_required=False)

class GenericSitemap(Sitemap):
    priority = None
    changefreq = None

    def __init__(self, info_dict, priority=None, changefreq=None):
        self.queryset = info_dict['queryset']
        self.date_field = info_dict.get('date_field', None)
        self.priority = priority
        self.changefreq = changefreq

    def items(self):
        # Make sure to return a clone; we don't want premature evaluation.
        return self.queryset.filter()

    def lastmod(self, item):
        if self.date_field is not None:
            return getattr(item, self.date_field)
        return None

A solução é bastante simples:

from django.contrib.sitemaps import GenericSitemap
from django.conf import settings

SITEMAP_LIMIT = getattr(settings, 'SITEMAP_LIMIT', 2000)

class LimitedSitemap(GenericSitemap):
    limit = SITEMAP_LIMIT

Em primeiro lugar eu faço uso da variável SITEMAP_LIMIT que deve ser setada no settings.py, caso ela não esteja setada o valor 2000 é usado.

Depois crio uma classe chamada LimitedSitemap que herda de GenericSitemap e faço uso do limite para paginação. Com um limite de 2000 objetos a consulta ao banco ficou e a renderização do template ficaram bem mais leves.

Setup inicial de um projeto Django 1.3

Depois de trabalhar em alguns projetos com o framework Django, eu passei a seguir uma série de padrões no setup inicial de uma aplicação, esse post descreve detalhadamente todo o processo.

Inicialmente eu tenho um diretório /home/allisson/Projects que é onde eu crio os meus projetos em Django, vamos começar criando um novo projeto:

$ cd Projects/
$ django-admin.py startproject teste
$ cd teste/
$ chmod +x manage.py # assim posso rodar ./manage.py no lugar de python manage.py

Inicialmente temos apenas quatro arquivos no diretório teste, vamos criar mais alguns arquivos e diretórios:

$ mkdir apps # diretório das aplicações do projeto
$ mkdir templates # diretório dos templates
$ mkdir media # diretório para os uploads de arquivos
$ mkdir static # diretório para uso do aplicativo staticfiles
$ mkdir sitestatic # diretório para arquivos estáticos do site (img, css, js)
$ touch apps/__init__.py # criando o arquivo init para deixar o diretório como um módulo em python

O principal arquivo é o settings.py, inicialmente ele tem essa estrutura:

# Django settings for teste project.

DEBUG = True
TEMPLATE_DEBUG = DEBUG

ADMINS = (
    # ('Your Name', 'your_email@example.com'),
)

MANAGERS = ADMINS

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': '',                      # Or path to database file if using sqlite3.
        'USER': '',                      # Not used with sqlite3.
        'PASSWORD': '',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                      # Set to empty string for default. Not used with sqlite3.
    }
}

# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'America/Chicago'

# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

SITE_ID = 1

# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True

# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale
USE_L10N = True

# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = ''

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = ''

# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = ''

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'

# URL prefix for admin static files -- CSS, JavaScript and images.
# Make sure to use a trailing slash.
# Examples: "http://foo.com/static/admin/", "/static/admin/".
ADMIN_MEDIA_PREFIX = '/static/admin/'

# Additional locations of static files
STATICFILES_DIRS = (
    # Put strings here, like "/home/html/static" or "C:/www/django/static".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
#    'django.contrib.staticfiles.finders.DefaultStorageFinder',
)

# Make this unique, and don't share it with anybody.
SECRET_KEY = 'wrqj$_b0lg!gfp+xlnn#zs^5vh*f0=+2e=e@4*=)&*f*4_wt_f'

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
#     'django.template.loaders.eggs.Loader',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
)

ROOT_URLCONF = 'teste.urls'

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
)

# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'mail_admins': {
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': True,
        },
    }
}

No começo desse arquivo eu insiro o seguinte código:

import sys
import os.path

PROJECT_PATH = os.path.dirname(os.path.abspath(__file__))

sys.path.insert(0, os.path.join(PROJECT_PATH, 'apps'))

Esse trecho de código serve para duas finalidades:

1) Armazenar na variável PROJECT_PATH o diretório atual do seu projeto, isso é bastante útil para setar os caminhos para diretórios de templates e etc.

2) O diretório apps/ é adicionado no PATH do python, dessa forma se eu tiver um aplicativo em apps/meuapp eu posso chamar meuapp.models no lugar de teste.apps.meuapp.models.

Alterando a configuração do banco de dados:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(PROJECT_PATH, 'teste.sqlite'),
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}

Caso você alterasse o ‘NAME’ para ‘teste.sqlite’ rodaria sem problemas localmente (pelo ./manage.py runserver), o problema ocorre quando você faz o deploy dessa aplicação onde é necessário o caminho completo do banco sqlite para funcionar corretamente. Essa configuração vai funcionar para os dois casos.

Alterando o MEDIA_ROOT e MEDIA_URL:

# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media')

# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = '/media/'

O MEDIA_ROOT guarda os arquivos que os usuários enviam para o site, usei o PROJECT_PATH novamente pelos mesmos motivos do banco de dados, não importa pra onde eu mova o meu projeto ele sempre vai apontar para o diretório correto.

Alterando o STATIC_ROOT e STATIC_URL:

# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = os.path.join(PROJECT_PATH, 'static')

# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'

O STATIC_ROOT guarda os arquivos estáticos do projeto (imagens, css, js etc) que são processados pelo aplicativo staticfiles (basicamente o staticfiles coleta todos os arquivos estáticos e armazena nesse diretório).

Alterando o STATICFILES_DIRS:

# Additional locations of static files
STATICFILES_DIRS = (
    # Put strings here, like "/home/html/static" or "C:/www/django/static".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    ('site', os.path.join(PROJECT_PATH, 'sitestatic')),
)

Ao adicionar o sitestatic/ com o prefixo site informamos ao aplicativo staticfiles que adicione os arquivos desse diretório na url /static/site/, se for criado um arquivo sitestatic/file.txt ele vai poder ser acesso via url /static/site/file.txt

Alterando o TEMPLATE_DIRS:

TEMPLATE_DIRS = (
    os.path.join(PROJECT_PATH, 'templates'),
)

Essas são as alterações básicas no settings.py, além disso eu adiciono o seguinte trecho no final:

# ==============================================================================
# Local settings here
# ==============================================================================

# Flag to serve static files under media/ and static/
SERVE_STATIC_FILES = True

# ==============================================================================
# Load settings_local.py if exists
# ==============================================================================
try:
    execfile(os.path.join(PROJECT_PATH, 'settings_local.py'), globals(), locals())
except IOError:
    pass

Aqui temos duas funcionalidades:

1) SERVE_STATIC_FILES é uma flag criada para servir ou não arquivos estáticos dos diretórios media/ e static/ pelo Django. Eu deixo como True porque em ambiente de desenvolvimento nem sempre queremos subir um Apache/Nginx para servir esses arquivos. Eu também já vi gente usar o DEBUG como flag para isso, mas eu prefiro setar essa flag porque pode ocorrer que eu queira o DEBUG ativo e os arquivos estáticos não sendo servidos pelo Django.

2) Se existir um arquivo settings_local.py carregue o conteúdo dele, isso é bastante útil para modificações pessoais, por exemplo, eu quero rodar o mysql no lugar do sqlite, então eu vou e crio um arquivo settings_local.py com o seguinte conteúdo:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': 'teste', # Or path to database file if using sqlite3.
        'USER': 'root',                      # Not used with sqlite3.
        'PASSWORD': 'root',                  # Not used with sqlite3.
        'HOST': '',                      # Set to empty string for localhost. Not used with sqlite3.
    }
}

Quando eu rodar o ./manage.py runserver ele vai usar o mysql no lugar o sqlite que foi definido no settings.py.

Agora porque isso é útil? Quando for compartilhar esse código com outras pessoas via algum scm (git/mercurial/subversion), você vai deixar o settings.py padrão e caso queira fazer alguma modificação pessoal basta criar um settings_local.py e fica tudo resolvido. Claro que ao usar um scm você deve colocar o settings_local.py no ignore do scm, evitando que ele seja enviado por acidente para o repositório.

Alterando o urls.py:

from django.conf.urls.defaults import patterns, include, url
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.conf import settings

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
)

if settings.SERVE_STATIC_FILES:
    urlpatterns += patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve', 
            {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
    )
    # staticfiles
    urlpatterns += staticfiles_urlpatterns()

Nesse urls.py eu adicionei a rota para aplicação admin (lembre de adicionar o admin na lista de apps do seu projeto) e no final do arquivo eu checo se a flag SERVE_STATIC_FILES é True, caso seja eu adiciono as rotas para servir arquivos estáticos pelas urls /media/ e /static/

Para configurações de deploy eu crio um arquivo settings_production.py:

# Load settings first
try:
    from settings import *
except ImportError:
    pass
 
# Now override any of them
DEBUG = False
SERVE_STATIC_FILES = False

Esse arquivo importa todas as configurações do settings.py e a partir disso você pode sobrescrever alterações de forma semelhante ao settings_local.py, a diferença é que o settings_production.py deve ser enviado ao repositório scm. Nesse exemplo eu desliguei as flags de DEBUG e SERVE_STATIC_FILES.

Para parte de testes eu crio um arquivo runtests:

#!/bin/sh
./manage.py test meuapp

Depois eu deixo esse arquivo com permissão de execução:

$ chmod +x runtests

O problema quando você roda o ./manage.py test é que ele vai testar os seus aplicativos e praticamente todo o framework Django e aplicativos de terceiros.

Claro que você não necessita rodar os testes do Django e de aplicativos de terceiros, os únicos testes importantes são os dos seus aplicativos.

O comando ./manage.py test meuapp roda os testes para o aplicativo meuapp, agora imagine que você tem vinte aplicativos no seu projeto, se você for rodar esse comando vai ser difícil lembrar de todos os aplicativos, então pra resolver isso eu crio um arquivo runtests onde eu sempre fico atualizando o comando a medida que novos aplicativos são criados ou removidos.

E por final eu crio um arquivo requirements.txt:

Django==1.3
MySQL-python>=1.2.2
PIL>=1.1.7

Nesse arquivo eu informo as depêndencias do meu projeto, nesse caso eu coloquei como dependências o Django versão 1.3, o MySQL-python versão 1.2.2 ou maior e o PIL (python-imaging) versão 1.1.7 ou maior.

Quando alguem baixar o código do projeto, basta rodar o comando:

$ pip install -r requirements.txt

Com esse comando o pip vai ler esse arquivo e instalar todas as bibliotecas listadas nele.

É isso aí pessoal, esse é o meu setup básico para novas aplicações em Django 1.3.