Escrevendo sua primeira aplicação Django, parte 3

Este tutorial inicia-se onde o Tutorial 2 terminou. Vamos continuar a aplicação web de enquete e focaremos na criação da interface pública – “views”.

Filosofia

Uma view é um “tipo” de página em sua aplicação Django que em geral serve uma função específica e tem um template específico. Por exemplo, em uma aplicação de blog, você deve ter as seguintes views:

  • Página inicial do blog - exibe os artigos mais recentes;
  • Página de “detalhes” - página de detalhamento de um único artigo;
  • Página de arquivo por ano - exibe todos os meses com artigos de um determinado ano;
  • Página de arquivo por mês - exibe todos os dias com artigos de um determinado mês;
  • Página de arquivo por dia - exibe todos os artigos de um determinado dia;
  • Ação de comentários - controla o envio de comentários para um artigo.

Em nossa aplicação de enquetes, nós teremos as seguintes views:

  • Página de “arquivo” de enquetes - exibe as enquetes mais recentes;
  • Página de “detalhes” da enquete - exibe questões para a enquete, sem os resultados mas com um formulário para votar;
  • Página de “resultados” de enquetes - exibe os resultados de uma enquete em particular;
  • Ação de voto - permite a votação para uma escolha particular em uma enquete em particular.

No Django, cada view é representada por uma função simples em Python.

Monte suas URLs

O primeiro passo para escrever views é montar sua estrutura de URLs. Você faz isso criando um módulo em Python, chamado de URLconf. URLconfs são como o Django associa uma URL a um código em Python.

Quando um usuário requisita uma página construída em Django, o sistema verifica a variável ROOT_URLCONF, que contém uma string do caminho de um módulo Python. O Django carrega esse módulo e verifica se o mesmo possui uma variável chamada urlpatterns, que é uma sequência de tuplas no seguinte formato:

(expressão regular, função Python de resposta [, dicionário opcional])

O Django começa pela primeira expressão regular e percorre até o final da lista, comparando a URL requisitada contra cada expressão regular até encontrar uma que combine.

Quando encontra uma que combine, o Django chama a função Python de resposta, com um objeto HttpRequest como seu primeiro argumento, quaisquer valores “capturados” da expressão regular como argumentos chave e opcionalmente, os elementos chaves do dicionário informado na URL (um terceiro item opcional da tupla).

Para saber mais sobre objetos HttpRequest, veja a documentação sobre Objetos de requisição e resposta. Para maiores detalhes sobre a URLconf, veja URL dispatcher.

Quando você executou django-admin.py startproject mysite no início do Tutorial 1, ele criou uma URLconf padrão em mysite/urls.py. E também fixou automaticamente a variável ROOT_URLCONF (em settings.py) para apontar para este arquivo:

ROOT_URLCONF = 'mysite.urls'

Pausa para um exemplo. Edite mysite/urls.py e deixe como abaixo:

from django.conf.urls.defaults import *

from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    (r'^polls/$', 'mysite.polls.views.index'),
    (r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
    (r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
    (r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
    (r'^admin/(.*)', admin.site.root),
)

Aqui vale uma revisão. Quando alguém solicita uma página do seu web site – diz, “/polls/23/”, o Django carregará este módulo em Python, porque ele foi apontado pela variável ROOT_URLCONF. Ele acha a variável chamada urlpatterns e percorre as expressões regulares na ordem. Quando ele encontra uma expressão regular que combine – r'^polls/(?P<poll_id>\d+)/$' – ele carrega a função detail() de mysite.polls.views.detail. Que corresponde à função detail() em mysite/polls/views.py. Finalmente, ele chama a função detail() como a seguir:

detail(request=<HttpRequest object>, poll_id='23')

A parte poll_id=‘23’` vem de (?P<poll_id>\d+). Usando parênteses em torno de um padrão “captura” o texto casado por este padrão e envia-o como um argumento da função; a ?P<poll_id> define o nome que será usado para identificar o padrão casado; e \d+ é a expressão regular para casar uma sequência de dígitos (ex., um número).

Como os padrões de URL são expressões regulares, realmente não há limites para o que você possa fazer com elas. E também não é necessário adicionar extensão na URL como .php - a menos que você tenha um sinistro senso de humor, neste caso você pode fazer algo como:

(r'^polls/latest\.php$', 'mysite.polls.views.index'),

Mas, não o faça isso, Isto é idiota.

Note que aquelas expressões regulares não procuram por parâmetros de GET e POST ou nome de domínios. Por exemplo, em uma requisição para http://www.example.com/myapp/, a URLconf irá procurar por /myapp/. Numa requisição para ``http://www.example.com/myapp/?page=3, a URLconf irá procurar por /myapp/.

Se você precisar de ajuda com expressões regulares, veja nesta pagina da Wikipedia e na documentação do Python. Também o livro da “O’Reilly “Mastering Regular Expressions”, de Jeffrey Friedl que é fantástico.

Finalmente, uma nota de performance: essas expressões regulares são compiladas na primeira vez que o módulo URLconf é carregado. Elas são super rápidas.

Escreva sua primeira view

Bem, nós não criamos nenhuma view ainda – nós só temos a URLconf. Mas vamos ter certeza de que o Django está seguindo a URLconf apropriadamente.

Rode o servidor web de desenvolvimento do Django:

python manage.py runserver

Agora vá para “http://localhost:8000/polls/” no seu domínio em seu navegador web. Você deverá ver uma amigavelmente colorida página de erro com a seguinte mensagem:

ViewDoesNotExist at /polls/

Tried index in module mysite.polls.views. Error was: 'module'
object has no attribute 'index'

Este erro ocorreu porque você não escreveu uma função index() no módulo mysite/polls/views.py.

Tente “/polls/23/”, “/polls/23/results/” e “/polls/23/vote/”. As mensagens de erro dirão a você qual view o Django tentou (e não conseguiu encontrar, porque você não a escreveu nenhuma view ainda).

Hora de criar a primeira view. Abra o arquivo mysite/polls/views.py e ponha o seguinte código em Python dentro dele:

from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the poll index.")

Esta é a view mais simples possível. Vá para /polls/ em seu navegador, e você irá ver seu texto.

Agora adicione a seguinte view. Esta é ligeiramente diferente, porque ela recebe um argumento (que, lembre-se, é passado com o que foi capturado pela expressão regular na URLconf):

def detail(request, poll_id):
    return HttpResponse("You're looking at poll %s." % poll_id)

Dê uma olhada no seu navegador, em /polls/34/. Ele vai mostrar o ID que você informou na URL.

Escreva views que façam algo

Cada view é responsável por fazer uma das duas coisas: retornar um objeto HttpResponse` contendo o conteúdo para a página requisitada, ou levantar uma exceção como Http404. O resto é com você.

Sua view pode ler registros do banco de dados, ou não. Ela pode usar um sistema de templates como o do Django - ou outro sistema de templates Python de terceiros - ou não. Ele pode gerar um arquivo PDF, saída em um XML, criar um arquivo ZIP sob demanda, qualquer coisa que você quiser,usando qualquer biblioteca Python você quiser.

Tudo que o Django espera é que a view retorne um HttpResponse. Ou uma exceção.

Por conveniência, vamos usar a própria API de banco de dados do Django, que nós vimos no Tutorial 1. Aqui está uma tentativa para a view index(), que mostra as últimas 5 enquetes no sistema, separadas por vírgulas, de acordo com a data de publicação:

from mysite.polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    output = ', '.join([p.question for p in latest_poll_list])
    return HttpResponse(output)

Há um problema aqui, todavia: o design da página esta codificado na view. Se você quiser mudar a forma de apresentação de sua página, você terá de editar este código diretamente em Python. Então vamos usar o sistema de templates do Django para separar o design do codigo Python:

from django.template import Context, loader
from mysite.polls.models import Poll
from django.http import HttpResponse

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    t = loader.get_template('polls/index.html')
    c = Context({
        'latest_poll_list': latest_poll_list,
    })
    return HttpResponse(t.render(c))

Este código carrega o template chamado polls/index.html e passa para ele um contexto. O contexto é um dicionário mapeando nomes de variáveis do template para objetos Python.

Recarregue a página. Agora você verá um erro:

TemplateDoesNotExist at /polls/
polls/index.html

Ah. Não há template ainda. Primeiro, crie um diretório, em algum lugar de seu sistema de arquivos, Cujo o conteúdo o Django possa acessar. (O Django roda com qualquer usuário que seu servidor rodar.) Portanto não o crie dentro da raiz de documentos do seu servidor. Você provavelmente não gostaria torna-lo público, apenas por questões de segurança. Agora edite a variável TEMPLATE_DIRS em seu settings.py para dizer ao Django onde ele pode encontrar os templates - exatamente como você fez na seção “Customize a aparência do site de administracao” do Tutorial 2.

Quando você tiver feito isto, crie um diretório polls em seu diretório de templates. Dentro, crie um arquivo chamado index.html. Note que nosso código loader.get_template('polls/index.html') acima direciona para “[template_directory]/polls/index.html” no sistema de arquivos.

Ponha o seguinte código no arquivo do template:

{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li>{{ poll.question }}</li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

Carregue a página em seu navegador, e você verá uma lista contendo a enquete “What’s up” do Tutorial 1.

Um atalho: render_to_response()

É um estilo muito comum carregar um template, preenchê-lo com um contexto e retornar um objeto HttpResponse com o resultado do template renderizado. O Django fornece este atalho. Aqui esta toda a view index() reescrita:

from django.shortcuts import render_to_response
from mysite.polls.models import Poll

def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html', {'latest_poll_list': latest_poll_list})

Note que uma vez que você tiver feito isto em todas as views, nós não vamos mais precisar importar loader, Context e HttpResponse.

A função render_to_response() recebe o nome do template como primeiro argumento e um dicionário opcional como segundo argumento. Ele retorna um objeto HttpResponse do template informado renderizado com o contexto determinado.

Levantando exceção 404

Agora, vamos atacar a view de detalhe da enquete - a página que mostra as questões para uma enquete lançada. Aqui esta a view:

from django.http import Http404
# ...
def detail(request, poll_id):
    try:
        p = Poll.objects.get(pk=poll_id)
    except Poll.DoesNotExist:
        raise Http404
    return render_to_response('polls/detail.html', {'poll': p})

O novo conceito aqui: a view levanta a exceção Http404 se a enquete com ID requisitado não existir.

Nós iremos discutir o que você poderia colocar no template polls/detail.html mais tarde, mas se você gostaria de rapidamente ter o exemplo acima funcionando, é só:

{{ poll }}

irá ajudar por agora.

Um atalho: get_object_or_404()

É um estilo muito comum usar get() e levantar uma exceção Http404 se o objeto não existir. O Django fornece um atalho para isso. Aqui esta a view detail(), reescrita:

from django.shortcuts import render_to_response, get_object_or_404
# ...
def detail(request, poll_id):
    p = get_object_or_404(Poll, pk=poll_id)
    return render_to_response('polls/detail.html', {'poll': p})

A função get_object_or_404() recebe um model do Django como primeiro argumento e um número arbitrário de argumentos chave, que ele passa para a função do módulo get(). E levanta uma exceção Http404 se o objeto não existir.

Filosofia

Porquê usamos uma função auxiliar get_object_or_404() ao invés de automaticamente capturar as exceções ObjectDoesNotExist em alto nível ou fazer a API do model levantar Http404 ao invés de ObjectDoesNotExist?

Porque isso seria acoplar a camada de modelo com a camada de visão. Um dos principais objetivo do design do Django é manter o baixo acoplamento.

Há também a função get_list_or_404(), que trabalhada da mesma forma que get_object_or_404() – com a diferença de que ela usa filter() ao invés de get(). Ela levanta Http404 se a lista estiver vazia.

Escreva uma view do código 404 (página não encontrada)

Quando você levanta Http404 de dentro de uma view, o Django carregará uma view especial devotada para manipulação de erros 404. Ela é encontrada procurando pela variável handler404, que é uma string em Python com o caminho da view separada por pontos - o mesmo formato que a função de chamada das URLconf usa. Uma view 404 por sí só não tem nada de especial: é apenas uma view normal.

Você normalmente não terá de se incomodar em escrever views de 404. Por padrão, as URLconfs têm a seguinte linha no começo do arquivo:

from django.conf.urls.defaults import *

Que se encarrega de definir handler404 no módulo corrente. Como você pode ver em django/conf/urls/defaults.py, handler404 é definido para django.views.defaults.page_not_found() por padrão.

Mais quatro coisas a se notar sobre views 404:

  • Se o DEBUG for definido como True (no seu módulo settings) sua view 404 nunca será usada (e assim o seu template 404.html nunca será renderizado) porque o traceback será exibido em seu lugar.
  • A view 404 também é chamada se o Django não conseguir encontrar uma combinação depois de checar todas as expressões regulares da URLconf;
  • Se você não definir sua própria view 404 e simplesmente usar a padrão, que é recomendadavel – você terá uma obrigação: criar um template 404.html na raiz de seu diretório de templates. A view padrão de 404 irá usar este template para todos os erros 404;
  • Se o DEBUG for definido como False (no seu módulo settings) e se você não criou um arquivo 404.html, um Http500 será levantado em seu lugar. Então lembre-se de criar um 404.html.

Escreva uma view do código 500 (erro no servidor)

Similarmente, URLconfs pode definir uma handler500, que aponta para uma view a ser chamada em caso de erro no servidor. Erros de servidor acontecem quando você tem erros de tempo de execução no código da view.

Use o sistema de templates

De volta para a view detail() da nossa aplicação de enquentes. Dada a variável de contexto poll, aqui está como o template polls/detail.htm deve ser:

<h1>{{ poll.question }}</h1>
<ul>
{% for choice in poll.choice_set.all %}
    <li>{{ choice.choice }}</li>
{% endfor %}
</ul>

O sistema de templates usa uma sintaxe separada por pontos para acessar os atributos da variável. No exemplo de {{ poll.question }}, primeiro o Django procura por dicionário no objeto poll. Se isto falhar, ele tenta procurar por um atributo – que funciona, neste caso. Se a procura do atributo também falhar, ele irá tentar chamar o método question() no objeto poll.

A chamada do método acontece no laço {% for %}: poll.choice_set.all é interpretado como codigo Python poll.choice_set.all(), que retorna objetos Choice iteráveis que são suportado para ser usado na tag {% for %}.

Veja o guia de templates para maiores detalhes sobre templates.

Simplificando as URLconfs

Vamos tomar um tempo para brincar em torno das views e o sistema de templates. A medida que você edita a URLconf, você poderá notar uma leve redundância nela:

urlpatterns = patterns('',
    (r'^polls/$', 'mysite.polls.views.index'),
    (r'^polls/(?P<poll_id>\d+)/$', 'mysite.polls.views.detail'),
    (r'^polls/(?P<poll_id>\d+)/results/$', 'mysite.polls.views.results'),
    (r'^polls/(?P<poll_id>\d+)/vote/$', 'mysite.polls.views.vote'),
)

Isto é, mysite.polls.views está em todas as chamadas.

Porque este é um caso comum, o framework de URLconf provê um atalho para prefixos comuns. Você pode decompor em fatores os prefixos comuns e adicioná-los como primeiro argumento de patterns(), como abaixo:

urlpatterns = patterns('mysite.polls.views',
    (r'^polls/$', 'index'),
    (r'^polls/(?P<poll_id>\d+)/$', 'detail'),
    (r'^polls/(?P<poll_id>\d+)/results/$', 'results'),
    (r'^polls/(?P<poll_id>\d+)/vote/$', 'vote'),
)

Isto é funcionalmente idêntico a formatação anterior. Somente um pouco mais arrumado.

Desacoplando as URLconfs

Já que nós estamos aqui, nós devemos tomar um tempo para desacoplar as URLs da aplicação poll da configuração do nosso projeto Django. As aplicações no Django devem ser escritas para serem plugáveis – que, cada aplicação em particular deve ser transferível para qualquer outra instalação Django com o mínimo de esforço.

Nossa aplicação poll é agradavelmente desacoplada neste ponto, graças à estrita estrutura de diretórios que o comando python manage.py startapp criou, mas uma parte dela está acoplada às configuracoes do Django: A URLconf.

Nós vínhamos editando as URLs em mysite/urls.py, mas o projeto de URLs de uma aplicação é específicas para ela, não para a instalação do Django – vamos então mover as URLs para dentro do diretório da aplicação.

Copie o arquivo mysite/urls.py para mysite/polls/urls.py. Então, modifique o mysite/urls.py para remover as URLs específicas da aplicação poll e insira um include():

...
urlpatterns = patterns('',
    (r'^polls/', include('mysite.polls.urls')),
    ...

O include() simplesmente referencia outra URLconf. Observe que a expressão regular não tem um $ (caracter que combina com o fim da string) mas possui uma barra. Em qualquer momento o Django encontra o include(), ele decepa fora qualquer parte da URL que tiver combinado ate este ponto e envia a string restante para a URLconf incluída para um tratamento posterior.

Aqui o que acontece se um usuário vai para "/polls/34/" neste sistema:

  • O Django irá encontrar uma combinacao em '^polls/'
  • Ele irá separar o texto combinado ("polls/") e enviar o restante do texto – "34/" – para a URLconf ‘mysite.polls.urls’ para um tratamento posterior.

Agora que nós desacoplamos isto, nós precisamos desacoplar a URLconf ‘mysite.polls.urls’ removendo o trecho "polls/" inicial de cada linha, e removendo as linhas registrando o site administrativo:

urlpatterns = patterns('mysite.polls.views',
    (r'^$', 'index'),
    (r'^(?P<poll_id>\d+)/$', 'detail'),
    (r'^(?P<poll_id>\d+)/results/$', 'results'),
    (r'^(?P<poll_id>\d+)/vote/$', 'vote'),
)

A idéia por trás do include() e o desacoplamento da URLconf é criar facilmente URLs plug-and-play. Agora que as polls possuem sua própria URLconf, elas podem ser colocadas abaixo de "/polls/", ou abaixo de "/fun_polls/", ou abaixo de "/content_polls/", ou qualquer outra raiz de URL, que a aplicação irá continuar funcionando.

Toda a aplicação poll preocupa-se com URLs relativas, e não com URLs absolutas.

Quando você estiver confortável em escrever views, leia a a parte 4 deste tutorial para aprender sobre processamento de formulários simples e sobre as views genéricas.