terça-feira, 27 de março de 2018

Tutorial 10 de 10

Codificando uma app simples

Continuando a alteração no admin...

Personalizando a listagem da página de Administração:

Vamos fazer melhorias na página admin de Questão que exibe todas as Questões (enquetes) cadastradas.

Por padrão, o Django mostra o str() de cada objeto. Mas é possível mostrar campos individuais. Para tal, use a opção 'list_display', que é uma tupla de nomes de campos a serem exibidos, como colunas. Edite o arquivo admin.py, a classe QuestaoAdmin e insira a linha abaixo:

# ...
    list_display = 
    ('texto_questao', 'data_pub')

Vamos incluir também o método 'was_published_recently()':
# ...
    list_display = 
    ('texto_questao', 'data_pub', 'was_published_recently')

Você pode clicar no cabeçalho da coluna para ordená-las por estes valores – exceto no caso da coluna 'was_published_recently', porque a ordenação pelo resultado de um método arbitrário não é suportada. Também note que o cabeçalho da coluna para was_published_recently é, por padrão, o nome do método (com underscores substituídos por espaços), e que cada linha contem a representação da saída.
--------------------------------------------------------------------
Vamos melhorar esta exibição, adicionando atributos ao método em questão. Edite o arquivo models.py, como segue:

class Questao(models.Model):
    # ...
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.data_pub <= now
    was_published_recently.admin_order_field = 'data_pub'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Publicado Recentemente?'

Uma melhoria para a página de edição de 'Questao': 
Adicionar um filtro: edite o arquivo admin.py e adicione o código abaixo, dentro da classe 'QuestaoAdmin': 
... list_filter = ['data_pub']
...

Isso adicionará uma barra lateral 'Filtro' que permite filtrar a lista de edição pelo campo 'data_pub' ('Data de Publicação').
--------------------------------------------------------------------
O tipo do filtro apresentado depende do tipo do campo pelo qual você está filtrando. Por conta de data_pub ser uma instância da classe DateTimeField, o Django consegue deduzir que os filtros apropriados são: 
'Qualquer data'
'Hoje'
'Últimos 7 dias'
'Este mês'
'Este ano'
'Sem data'
'Tem data'

Agora, vamos adicionar capacidade de pesquisa:
Insira o código abaixo do código adicionado anteriormente:
search_fields = ['texto_questao']

Isso adiciona um campo de pesquisa ao topo da lista de edição. Quando alguém informa termos de pesquisa, o Django irá pesquisar o campo 'texto_questao'. Você pode usar quantos campos quiser, mas por ele usar uma consulta LIKE internamente, limitar o número de campos de pesquisa a um número razoável, será mais fácil para o seu banco de dados para fazer pesquisas.

Observe que a página de edição fornece uma paginação por padrão. O padrão é mostrar 100 itens por página. 
Paginação da página de edição, campos de pesquisa, filtros, hierarquia por data, e ordenação por cabeçalho de coluna, todos trabalham em sincronia como deveriam.
--------------------------------------------------------------------
Personalizando os templates do seu projeto:

Crie um diretório templates no diretório do seu projeto (aquela que contém manage.py). Os templates podem estar em qualquer lugar em seu sistema de arquivos que o Django possa acessar. (Django é executado como qualquer usuário que seu servidor é executado). No entanto, manter seus templates dentro do projeto é uma boa convenção a seguir.

Abra o arquivo settings.py e adicione uma opção DIRS, configuração TEMPLATES: mysite/settings.py
...
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #Esta Linha
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
...

DIRS é uma lista de diretórios de arquivos que serão checados quando o Django carregar seus templates, ou seja, caminhos de busca.
--------------------------------------------------------------------
Organizando templates:

Assim como os arquivos estáticos, poderíamos ter todos os nossos templates juntos no mesmo diretório. Contudo, templates que pertencem a um app devem ser colocados no diretório de templates da aplicação, exemplo: votacao/templates.

Onde estão os arquivos de código-fonte do Django?
Execute o seguinte comando:
python -c "import django; print(django.__path__)"

# Saída: ['C:\\marco\\venv\\lib\\site-packages\\django']
--------------------------------------------------------------------
Personalizando a aparência do Site de Administração:

Vamos fazer algumas alterações, como o título da área administrativa do Django, que atualmente está como 'Django administration' no topo página. Para esta alteração usaremos o sistema de templates do Django.
Obs.: A página de administração de Django é feita com o próprio Django e conseqüentemente sua interface usa o sistema de templates nativo do Django.

Crie um diretório admin dentro de templates, e copie o template admin/base_site.html de dentro do diretório padrão do site de administração do Django (django/contrib/admin/templates) para dentro deste diretório.
Agora, edite o arquivo base_site.html e substitua:

{{ site_header|default:_('Django administration') }} 

(incluindo as chaves) com nome do seu próprio site como desejar. 
Você deve acabar com uma seção de código como:

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">
Administração de Questões</a></h1>
{% endblock %}

Nós usamos esta abordagem para ensinar-lhe como substituir templates. Em um projeto real, você provavelmente usaria o atributo django.contrib.admin.AdminSite.site_header para fazer mais facilmente essa personalização em particular.

Esse template contém uma série de textos como {% block branding %} e {{ tittle }}. As tags {% e {{ fazem pate da linguagem de templates do Django. Quando o Django renderiza este template 'base_site.html', o seu conteúdo é processado por essa linguagem de templates para produzir a página HTML final.

Qualquer template padrão do site de administração pode ser sobrescrito. Para sobrescrever um template, apenas faça a mesma coisa que você fez com base_site.html, basta copiá-lo do diretório padrão para o seu próprio diretório, e fazer as mudanças.
--------------------------------------------------------------------
Personalizando templates da sua aplicação:

Obs.: Mas se DIRS estava vazio por padrão, como o Django pôde encontrar o diretório padrão dos templates do site de administração? 
A resposta é, Desde que APP_DIRS seja True, o Django irá automaticamente procurar por um subdiretório templates/ dentro de cada pacote de aplicação, para usar como fallback (django.contrib.admin é uma aplicação).

A aplicação enquete não é muito complexa e não precisa de templates personalizados para o site de administração. Mas se ele crescer e ficar mais sofisticado e necessária a modificação dos templates padrão de administração do Django para algumas de suas funcionalidades, seria mais sensato modificar os templates da aplicação, e não os do projeto. 

Dessa forma, você poderia incluir a aplicação de enquete em qualquer novo projeto e ter a certeza que iria encontrar os modelos personalizados que você precisa.
--------------------------------------------------------------------
Personalize a página inicial do site de administração:

De maneira similar, você pode querer personalizar a aparência da página inicial do site de administração do Django.
Por padrão, ele exibe todas as aplicações em INSTALLED_APPS, que estão registrados na aplicação de administração, em ordem alfabética. E você pode querer fazer alterações significativas no layout. Além do que, a página inicial é provavelmente a página mais importante da página de administração, e deve ser fácil de usar.
O template para personalizar é admin/index.html. (Faça o mesmo que admin/base_site.html na seção anterior – copie ele do diretório padrão para o seu diretório de template personalizado). Edite o arquivo e você verá que ele usa uma variável de template chamada app_list. Esta variável contém cada app instalada no Django. Ao invés de usar isto, você pode criar links hard-coded para páginas do admin específicas em qualquer forma que você achar melhor.

Fim do Tutorial.
Este tutorial foi estudado com base no Tutorial com o link abaixo.
https://docs.djangoproject.com/pt-br/2.0/intro/
Meus agradecimentos ao autor do Tutorial do site DjangoProject.com.
--------------------------------------------------------------------

segunda-feira, 26 de março de 2018

Tutorial 9 de 10

Codificando uma app simples

Usando o admin do Django:

Vamos iniciar registrando o model Questao no admin.py.
Edite o arquivo admin.py:

from django.contrib import admin
from .models import Questao
admin.site.register(Questao)

Através da linha admin.site.register(Questao), o Django constrói um formulário padrão para representá-lo. 

Se desejar customizar o formulário é necessário aprimorar a codificação do admin.py. Podemos fazer da seguinte forma:

from django.contrib import admin
from .models import Questao
class QuestaoAdmin(admin.ModelAdmin):
    fields = ['data_pub', 'texto_questao']
admin.site.register(Questao, QuestaoAdmin)

Este é o padrão, criar uma classe do model e passá-la como 2º argumento para o register().
Esta mudança codificada acima faz com que o campo 'Data de publicação' apareça acima do campo 'Texto Questão'.
--------------------------------------------------------------------
Em necessidade, é possível dividir o formulário em grupos.
Continue editando o admin.py e veja.

from django.contrib import admin
from .models import Questao
class QuestaoAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['texto_questao']}),
        ('Informações de Data', {'fields': ['data_pub']}),
    ]
admin.site.register(Questao, QuestaoAdmin)

O 1º elemento de cada tupla em fieldsets é o título do grupo.
--------------------------------------------------------------------
Adicionando objetos relacionados:

Incluímos a página de administração Questão, vamos adicionar também a página de administração Escolha.
A forma mais simples é registrar Escolha no site de Administração, como foi feito com Questão. Edite o arquivo admin.py:

from django.contrib import admin
from .models import Escolha, Questao
# ...
admin.site.register(Escolha)

Agora 'Escolhas' é uma opção disponível no site de administração do Django. 

No formulário de 'Adicionar Escolha' o campo 'Questão' é uma caixa de seleção contendo todas as Questões (enquetes) cadastradas no banco de dados. O Django sabe que uma ForeignKey deve ser representada no site de administração como um campo <select>. 

Observe também o link 'Adicionar Outro' ao lado de Questao'. Todo objeto com um relacionamento de chave estrangeira para outro exibe essa opção. Quando você clica em 'Adicionar Outro', você terá uma janela popup com o formulário 'Adicionar Questao'. Se você adicionar uma Questão na janela e clicar em 'Salvar', o Django salvará a Questão (enquete) no banco de dados e irá dinamicamente adicionar a opção já selecionada ao formulário 'Adicionar Escolha'.

Mas essa é uma maneira ineficiente de adicionar objetos Escolha ao sistema. Seria muito melhor se puder adicionar várias opções diretamente quando criasse um objeto Questao. Vamos fazer isso.

Editando arquivo admin.py, remova a chamada register() do modelo Escolha. Faça outras alterações como segue:

from django.contrib import admin
from .models import Escolha, Questao

class EscolhaInline(admin.StackedInline):
    model = Escolha
    extra = 3
class QuestaoAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['texto_questao']}),
        ('Informação de Data', {'fields': ['data_pub'], 'classes': ['collapse']}),
    ]
    inlines = [EscolhaInline]
admin.site.register(Questao, QuestaoAdmin)

Agora, objetos 'Escolha' são editados na mesma página de admin de 'Questao'. Por padrão, fornece campos para 3 escolhas, como especificado em 'extra'.

No final dos 3 blocos fornecidos para o cadastro de Escolha, tem um link 'Adicionar outra escolha'. Se clicar neste link, é adicionado um novo bloco de cadastro de 'Escolha'. Se desejar remover este bloco adicional, clique no X no topo direito do bloco acrescentado. O usuário não pode remover os 3 blocos originais. 
Desta forma, a tela de cadastro de Questão e Escolha relacionadas ocupa bastante espaço da tela. Mas o Django fornece outra forma de exibir cada objeto relacionado, em uma única linha.
Para fazer esta alteração, mude no arquivo admin.py a declaração abaixo: 
...
class EscolhaInline(admin.TabularInline):
    #...

Com o TabularInline (em vez de StackedInline), os objetos relacionados são exibidos de forma mais compacta, formatados em tabela.
Neste design, há uma coluna 'Apagar?', que permite a remoção de Escolhas, que já foram salvas. Para utilizá-la, basta selecionar esta opção e clicar em Salvar.
--------------------------------------------------------------------
Obs.: 
Em settings.py, podemos alterar versões de Idioma e Fuso horário do admin. Para isso, basta alterar as opções como abaixo:
...
LANGUAGE_CODE = 'pt-br'
TIME_ZONE = 'America/Sao_Paulo' 
...

Outra alteração importante é a versão Plural do nome dos Models.
Por padrão, o plural utilizado no admin é com o acréscimo de 'S' ao 'verbose_name'. Com base na classe 'Questao', teremos o plural no admin Questaos, sendo que o correto seria 'Questões'. Para fazer esta alteração, edite o arquivo models.py e acrescente o código abaixo dentro da classe Questao:
    class Meta:
        verbose_name = 'Questão'
        verbose_name_plural = 'Questões'
--------------------------------------------------------------------

sábado, 24 de março de 2018

Tutorial 8 de 10

Codificando uma app simples

Agora, vamos adicionar uma folha de estilo e uma imagem.

Além do HTML gerado pelo servidor, aplicações web normalmente precisam servir outros arquivos (imagens, JavaScript, CSS, etc). No Django, estes arquivos são chamados de  'arquivos estáticos'. Para projetos pequenos, isto não é um grande problema, porque você pode simplesmente manter os arquivos estáticos em algum lugar que o seu servidor web consegue encontrar. Entretanto, em grandes projetos, especialmente aqueles comprometidos com múltiplas apps, lidar com múltiplos conjuntos de arquivos estáticos provindos de cada app pode se tornar complicado.

É para isto que o Django traz o módulo django.contrib.staticfiles, localizado em settings.py (INSTALLED_APPS): ele coleciona os 'arquivos estáticos' de cada uma de suas aplicações (e qualquer outro lugar que você especifique) em um único local que pode ser facilmente servido em produção.
--------------------------------------------------------------------
Personalizando a aparência do app:

1º, vamos criar um diretório chamado 'static' no diretório votacao. O Django vai procurar os arquivos estáticos ali, de maneira similar a como o Django encontra os templates dentro de votacao/templates/.

A configuração STATICFILES_FINDERS do Django contém uma lista de buscadores que sabem como descobrir os arquivos estáticos de diversas fontes. Um dos padrões é o App DirectoriesFinder que procura por um subdiretório 'static' em cada uma das INSTALLED_APPS. O site admin usa a mesma estrutura de diretórios para os arquivos estáticos.

Com o diretório 'static' que você acabou de criar, crie outro diretório chamado votacao e com ele crie um arquivo: style.css. O Path deve ficar assim: votacao/static/votacao/style.css.

Por conta da forma que o buscador de arquivos estáticos App DirectoriesFinder funciona, você pode referenciar este arquivo estático no Django simplesmente como votacao/style.css, similarmente como você referência o caminho para templates.

Arquivo estático namespace: Assim como templates, podemos nos safar somente colocando os arquivos estáticos diretamente em votacao/static (ao invés de criar outro subdiretório votacao), mas não é uma boa ideia. O Django vai escolher o 1º o arquivo estático que encontrar com aquele nome, e se você tem um arquivo estático com aquele mesmo nome em uma aplicação diferente, o Django não conseguiria distinguir entre elas. Nós precisamos apontar o Django para o arquivo correto, e a forma mais fácil de garantir isso é por usar um namespacing com eles. Isto é, colocando aqueles arquivos estáticos dentro de outro diretório com o mesmo nome da aplicação.

Coloque o seguinte código na sua folha de estilo - style.css:

li a {
    color: green;
}

Em seguida, edite o arquivo templates/votacao/index.html e adicione o código abaixo no início do arquivo:

{% load static %}
< link rel="stylesheet" type="text/css" 
href="{% static 'votacao/style.css' %}" />

A tag {% static %} gera a URL absoluta para arquivos estáticos.
Acesse o endereço http://localhost:8000/votacao/ e verifique que os links das questões devem estar verdes.
--------------------------------------------------------------------
Adicionando uma imagem de fundo:

Crie um subdiretório para imagens: votacao/static/votacao/images.
Dentro deste diretório, coloque uma imagem: background.jpg. 

Edite o seu stylesheet (votacao/static/votacao/style.css):

body {
    background: white url("images/background.jpg") no-repeat;
}

Recarregue http://localhost:8000/votacao/ e agora deve aparecer o fundo carregado no topo do lado esquerdo da tela.
--------------------------------------------------------------------

sexta-feira, 23 de março de 2018

Tutorial 7 de 10

Codificando uma app simples

Testes automatizados:
--------------------------------------------------------------------
O que são? Para que servem? É realmente útil? Como fazer?

São rotinas simples que checam o funcionamento do código.
Atuam em diferentes níveis, desde o model (verifica se o método retorna valor esperado), até o funcionamento global da aplicação (sequencias de entradas do usuário produz o resultado desejado).

São automatizados porque a própria app faz os testes para o programador. O programador cria os testes apenas uma vez, pode modificá-los se necessário, e os utiliza para checar o funcionamento do código, sem ter que gastar tempo executando testes manuais.

Testes são úteis para ganhar tempo em retestes. Verificar o que parece funcionar será sempre importante. Em uma grande aplicação será um grande ganho de tempo. Após uma alteração importantes de um componente, é importante executar testes para avaliação. Geralmente, os testes automatizados fazem dezenas de testes em segundos. Caso algo esteja com erros, será fácil identificar a origem.

Escrever testes pode ser uma tarefa monótona quando sabemos que está tudo funcionando corretamente. Mas é necessário, para evitar tempo gasto com testes.

Sem testes, o objetivo ou comportamento desejado de uma aplicação pode não ser tão claro. 
Testes mudam isso; eles mostram seu código por dentro, e quando algo sai errado, eles revelam parte do erro.

Testes fazem seu código mais atraente: Você pode criar uma parte importante no software, mas você vai encontrar muitos outros desenvolvedores que vão simplesmente ignorar ao olhar isto, por que isto carece de testes, e sem testes, não há confiança. 
Assim, esses outros desenvolvedores procuram ver os testes do seu software antes de levar a sério, e isso é uma outra razão para você iniciar a escrita dos testes.

Testes ajudam as equipes a trabalharem juntos: Os pontos anteriores foram escritos do ponto de vista de um único desenvolvedor mantendo uma aplicação. Aplicações complexas serão mantidas por times. Testes garantem que seus colegas de trabalho não quebrem acidentalmente o seu código (e que você não quebre o deles sem saber). Se você deseja trabalhar como um programador Django, deve ser bom escrevendo testes.
--------------------------------------------------------------------
Estratégias básicas de testes:

Há várias maneiras de abordar a escrita de testes.
Existe a disciplina chamada 'test-driven development' (TDD): é uma forma de programar onde o programador codifica testes antes de escrever o código. Isso talvez pareça não intuitivo, mas na verdade é similar o que muitas pessoas farão de qualquer forma: eles descrevem um problema, então criam um código para resolver isso. O desenvolvimento Test-driven simplesmente formaliza o problema em um caso de teste em Python.
Comumente, um novato em testes irá criar algum código e depois decidir que ele deveria ser testado. Talvez seria melhor escrever alguns testes antes, mas nunca é tarde para começar.
--------------------------------------------------------------------
Codificando o 1º teste automatizado:

Felizmente, há um pequeno bug na aplicação votacao para corrigirmos imediatamente: O método Questao.was_published_recently() retorna True se a Questao foi publicada dentro do último dia (o que está correto), mas também se o campo data_pub de Questao está no futuro (o que certamente não é o caso).
Para checar se o bug realmente existe, usando o Admin crie uma enquete na qual a data encontra-se no futuro e verifique o método através do shell (de dentro do diretório da app):
python manage.py shell

Digite:
import datetime
from django.utils import timezone
from votacao.models import Questao
#cria future_question com data 30 dias no futuro
future_question = Questao(data_pub=timezone.now() + datetime.timedelta(days=30))
#future_question foi publicada recentemente?
future_question.was_published_recently() 
#Resultado na tela: True

Observe que se a data foi de 30 dias no futuro, não pode ter sido publicada recentemente. Isto é claramente um erro.
--------------------------------------------------------------------
Criando um teste:
O que fizemos acima no shell para testar o problema é exatamente o que podemos fazer em um teste automatizado.
O lugar padrão para os testes de uma app é o arquivo tests.py.
O sistema de testes irá encontrar automaticamente todos os testes que estiverem em qualquer arquivo cujo o nome comece com test.
Coloque o seguinte código no arquivo tests.py na aplicação votacao:

import datetime

from django.utils import timezone
from django.test import TestCase

from .models import Questao

class QuestaoModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):

        """
        was_published_recently() retorna False para questoes que a data_pub está no futuro.
        """

        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Questao(data_pub=time)
        self.assertIs(future_question.was_published_recently(), False)


Criamos uma subclasse django.test.TestCase com o método que cria uma instância de Questiao com data_pub no futuro. Vamos checar a saída de was_published_recently(): que deve ser 'False'.
--------------------------------------------------------------------
Executando o teste:
No terminal, nós podemos executar nosso test:
$ python manage.py test votacao

E você verá algo como:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
====================================================================
FAIL: test_was_published_recently_with_future_question (votacao.tests.QuestaoModelTests)
--------------------------------------------------------------------
Traceback (most recent call last):
  File "/path/to/mysite/enquetes/tests.py", line 21, in test_was_published_recently_with_future_question
    self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

--------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
Destroying test database for alias 'default'...
--------------------------------------------------------------------
O que ocorreu foi o seguinte:

O comando 'python manage.py test votacao' procurou por testes na aplicação votacao.
Encontrou uma subclasse da classe django.test.TestCase.
Criou um banco de dados especial com o propósito de teste.
Procurou por métodos de test - aqueles cujos nomes começam com test.
Em test_was_published_recently_with_future_question é criado uma instância de Questao, definindo o campo data_pub para estar com 30 dias no futuro.
Usou o método assertIs(), para descobrir que o método was_published_recently() retorna True, mas queremos que retorne False.


O teste nos informa que o teste falhou e até mesmo a linha na qual a falha ocorreu.
--------------------------------------------------------------------
Corrigindo o erro:
Já sabemos onde está o problema. 
'Questao.was_published_recently()' deveria retornar 'False' se 'data_pub' está no futuro. 
Vamos alterar o método em models.py, dessa forma ele só retornará 'True' se a data também estiver no passado:

def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.data_pub <= now

Execute o teste novamente: python manage.py test votacao

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
--------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Destroying test database for alias 'default'...
--------------------------------------------------------------------
Depois de identificar um erro, nós escrevemos um teste que demonstra e corrige o erro no código, então o nosso teste passa.
Muitas outras coisas podem dar errado com a nossa aplicação no futuro, mas você pode ter certeza que não vamos reintroduzir este bug inadvertidamente, porque simplesmente rodando os testes já receberemos o aviso imediatamente. Nós podemos considerar que esta pequena parte da aplicação está a salvo para sempre.
--------------------------------------------------------------------
Testes mais abrangentes:

Vamos aprofundar nos testes do método was_published_recently().
Adicione mais 2 métodos de teste na mesma classe.
Codifique: votacao/tests.py

def test_was_published_recently_with_old_question(self):
    """
    was_published_recently() retorna False para questoes que data_pub é mais antiga que 1 dia.
    """

    time = timezone.now() - datetime.timedelta(days=1, seconds=1)
    old_question = Questao(data_pub=time)
    self.assertIs(old_question.was_published_recently(), False)


def test_was_published_recently_with_recent_question(self):
    """
    was_published_recently() retorna True para questoes que data_pub esteja no último dia.
    """

    time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
    recent_question = Questao(data_pub=time)
    self.assertIs(recent_question.was_published_recently(), True)   

Execute no prompt: python manage.py test votacao

Foram executados 3 testes.

Temos então, 3 testes que confirmam que was_published_recently() retorna valores sensíveis de perguntas do passado, recente e futuro.

Agora temos alguma garantia que o método testado acima vai se comportar de maneira esperada, dentro dos testes.
--------------------------------------------------------------------
Testando uma View

Atualmente, a app enquete irá publicar qualquer questão, inclusive as que o campo data_pub esteja no futuro. Vamos melhorar isso:  Definindo um data_pub no futuro deveria fazer com que a Questao seja publicada, mas que esteja invisível até lá.

No 1º teste, focamos no comportamento interno do código. Para este teste, queremos checar seu comportamento como seria experienciado por um usuário através de um navegador web.


o Django fornece um 'test Client' para simular um usuário interagindo com o código no nível de view. Podemos usá-lo em tests.py ou no shell.



Começaremos com o shell, onde faremos algumas coisas que não seriam necessárias no tests.py. A 1ª é definir o ambiente de teste no django:shell:



from django.test.utils import setup_test_environment
setup_test_environment()

O setup_test_environment() instala um renderizador de templates o qual permite que sejam examinados alguns atributos adicionais nas respostas http tal como um response.context que de outra maneira não estariam disponíveis. Este método não configura um banco de dados de teste, o que se segue será executado no banco de dados existente e a saída pode ser diferente quanto as questões que você já criou. Você talvez tenha resultados inesperados se seu TIME_ZONE no settings.py não estiver correto. Se você não lembrou de defini-lo antes, verifique-o antes de continuar.
Agora precisamos importar a classe cliente de teste (depois iremos usar a classe django.test.TestCase no tests.py, a qual traz seu próprio cliente, então isso não será necessário):

from django.test import Client
# cria uma instancia de Client para nosso uso
client = Client()

Agora, podemos pedir que o cliente faça algum trabalho para a gente:
# busque um response de '/'
response = client.get('/')
#Saída: Not Found: /
# devemos esperar um 404 deste endereço; se em vez de ver um erro "Invalid HTTP_HOST header" ver um 400 response, você provavelmente omitiu a chamada de setup_test_environment() descrita acima.

response.status_code
#Saída: 404
# De outra forma, devemos esperar encontarr algo como '/votacao/'
# Nós usaremos 'reverse()' em vez de um URL codificado

from django.urls import reverse
response = client.get(reverse('votacao:index'))
response.status_code
#Saída: 200

response.content
#Saída: "b'\n\n\n    <ul>\n\n    \n\n\t\t<!--<li><a href="/votacao/1/">Novidade?</a></li>
 -->\n        <li><a href="/votacao/1/">Novidade?</a></li>\n\t\t\n    \n\n\t\t<!
--<li><a href="/votacao/2/">Nova Quest\xc3\xa3o</a></li> -->\n        <li><a hre
f="/votacao/2/">Nova Quest\xc3\xa3o</a></li>\n\t\t\n    \n\n    </ul>\n\n'"
--------------------------------------------------------------------
Melhorando a nossa view:
A lista de enquete mostra enquetes que não estão publicadas ainda (isto é aqueles que tem data_pub no futuro). Vamos consertar isso.
Antes, introduzimos o class-based views, baseado no ListView:
votacao/views.py
class IndexView(generic.ListView):
    template_name = 'votacao/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-data_pub')[:5]

Precisamos fazer uma adição no método get_queryset() e alterá-lo de modo que ele verifique também a data comparando com timezone.now(). 

Codificando votacao/views.py:
...
from django.utils import timezone
...
    def get_queryset(self):
        """Retorna as últimas 5 questões publicadas."""
        #return Questao.objects.order_by('-data_pub')[:5]        

        """Return the last five published questions (not including those set to be published in the future)."""
        return Questao.objects.filter(data_pub__lte=timezone.now()).order_by('-data_pub')[:5]

Questao.objects.filter(data_pub__lte=timezone.now()): retorna um queryset contendo Questoes cujo o data_pub é menor ou igual a - que quer dizer, anterior ou igual a - timezone.now.
--------------------------------------------------------------------
Observações:
Queries:
>/GREATER THAN e </LESS THAN: gt, gte, lt, lte

As instruções SQL WHERE associadas a campos numéricos geralmente usam operadores matemáticos >, >=, =, <, <=, para restringir consultas a um determinado intervalo de números. Os modelos do Django suportam o uso de operadores matemáticos como segue:
  • >  : gt
  • >= : gte
  • <  : lt
  • <= : lte
Veja os exemplos abaixo:
from coffeehouse.items.models import Item

# Get Item records with stock > 5
Item.objects.filter(stock__gt=5)

# Get Item records with stock > or equal 10
Item.objects.filter(stock__gte=10)

# Get Item records with stock < 100
Item.objects.filter(stock__lt=100)

# Get Item records with stock < or equal 50
Item.objects.filter(stock__lte=50)
--------------------------------------------------------------------
Testando a nossa nova view:

Vamos criar um teste, baseado na sessão shell acima; Crie uma função de atalho para criar perguntas assim como uma nova classe de teste. Codifique:

...
from django.urls import reverse

def create_question(texto_questao, days):
    """
    Cria uma questao com o dado `texto_questao` e publique-o com
    determinado número de dias de compensação até agora (negativo para questões publicadas
    no passado, positivas para questões que ainda não foram publicadas).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Questao.objects.create(texto_questao=texto_questao, data_pub=time)

class QuestionIndexViewTests(TestCase):

    def test_no_questions(self):
        """
        Se nenhuma questão existe, uma mensagem apropriada é exibida.
        """
        response = self.client.get(reverse('votacao:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Nenhuma votacao está disponível.")
        self.assertQuerysetEqual(response.context['latest_questao_list'], [])

    def test_past_question(self):
        """
        Questoes com o data_pub no passado são mostradas na página index.
        """
        create_question(texto_questao="Past question.", days=-30)
        response = self.client.get(reverse('votacao:index'))
        self.assertQuerysetEqual(
            response.context['latest_questao_list'],
            ['<Questao: Past question.>']
        )

    def test_future_question(self):
        """
        Questões com o data_pub no futuro não são mostradas na página index.
        """
        create_question(texto_questao="Future question.", days=30)
        response = self.client.get(reverse('votacao:index'))
        self.assertContains(response, "Nenhuma votacao está disponível.")
        self.assertQuerysetEqual(response.context['latest_questao_list'], [])

    def test_future_question_and_past_question(self):
        """
        Mesmo que existam questões no passado e no futuro, apenas perguntas no passado são exibidas.
        """
        create_question(texto_questao="Past question.", days=-30)
        create_question(texto_questao="Future question.", days=30)
        response = self.client.get(reverse('votacao:index'))
        self.assertQuerysetEqual(
            response.context['latest_questao_list'],
            ['<Questao: Past question.>']
        )

    def test_two_past_questions(self):
        """
        As questões na página index podem exibir várias questões.
        The questions index page may display multiple questions.
        """
        create_question(texto_questao="Past question 1.", days=-30)
        create_question(texto_questao="Past question 2.", days=-5)
        response = self.client.get(reverse('votacao:index'))
        self.assertQuerysetEqual(
            response.context['latest_questao_list'],
            ['<Questao: Past question 2.>', '<Questao: Past question 1.>']
        )
--------------------------------------------------------------------
Explicações:

Começamos com uma função de atalho para questão, 'create_question', para retirar algumas repetições do processo de criar questões.

A função 'test_no_questions' não cria nenhuma questão, apenas verifica se precisa exibir a mensagem: 'Nenhuma votação está disponível', e verifica se 'latest_question_list' está vazio.

Note que a classe django.test.TestCase fornece alguns métodos adicionais de assertion. 
Nestes exemplos, usamos assertContains() e assertQuerysetEqual().

Em test_past_question, nós criamos uma questão e verificamos o que aparece na lista.

Em test_future_question, nós criamos uma questão com o data_pub no futuro. 

O banco de dados é resetado para cada método de teste. Então a primeira questão não está mais lá, e então novo a página index não deve exibir nenhuma questão nela.

E assim por diante. De fato, estamos usando os testes para contar uma história da entrada do admin e da experiencia do usuário no site, e verificando se para cada estado e para cada nova alteração no estado do sistema, o resultado esperado é publicado.
--------------------------------------------------------------------
Testando o DetailView:

Mesmo quando questões futuras não aparecem no index, os usuários ainda podem acessá-las se eles conhecem ou adivinham a URL correta. Então precisamos adicionar uma regra similar ao DetailView em votacao/views.py:

class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        """
        Exclui qualquer questão que nao foi publicada ainda..
        """
        return Questao.objects.filter(data_pub__lte=timezone.now())

Agora, adicionaremos mais testes para verificar que uma Questao, a qual seu data_pub está no passado pode ser mostrada, e que uma com data_pub no futuro não pode. Codifique votacao/tests.py:

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        O detail view de uma questao com o data_pub no futuro retorna um 404 (not found).
        """
        future_question = create_question(texto_questao='Future question.', days=5)
        url = reverse('votacao:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        O detail view de uma questao com o data_pub no passado mostra o texto da questao.
        """
        past_question = create_question(texto_questao='Past Question.', days=-5)
        url = reverse('votacao:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.texto_questao)      
--------------------------------------------------------------------
Ideias para mais testes:

Devemos adicionar um método similar ao get_queryset ao ResultsView e criar uma nova classe de teste para essa view. Isso será muito parecido com o que criamos há pouco; defato terá muitas repetições.
Podemos também melhorar nossa aplicação de várias outras maneiras, adicionando testes no caminho. Por exemplo é estranho que Questoes possam ser publicadas no site sem que tenham Escolhas. Então, nossas views podem verificar isso, e excluir tais Questoes. Nossos testes podem criar uma Questao sem Escolha e então testar que não estão publicadas, também criar uma Questao parecida com Escolhas, e testar se estão publicadas.

Talvez usuários registrados no admin devam ser permitidos a ver Questoes não publicadas, mas não os visitantes comuns. Independente da necessidade a ser adicionada para que isso seja realizado, deve ser acompanhada por um teste, não importando se você escreve o teste primeiro e faz o código para passar no teste, ou escreva o código da lógica primeiro e escreva o teste para fazer a prova.
--------------------------------------------------------------------
Quando estiver testando, mais é melhor:

Pode parecer que nossos testes estão crescendo fora de controle. Nessa taxa teremos logo mais código nos testes do que na aplicação, e a repetição não é muito estética, comparado a concisa elegância do resto do código.
Isso não importa. Deixe eles crescerem. Por boa parte do tempo, você pode escrever um teste uma vez e então esquecer dele. Ele vai continuar a executar sua função útil enquanto você continua a escrever a app.

Algumas vezes os testes precisarão ser atualizados. Suponha que emendamos as views para que somente Questoes com Escolhas são publicadas. Neste caso, muitos dos nossos testes existentes irão falhar - dizendo exatamente quais testes precisam ter ajustes para que sejam atualizados, assim que os próprios testes auxiliam na manutenção deles mesmos.
Na pior das hipóteses, ao continuar a desenvolver, talvez encontre alguns testes que são redundantes agora. Mas isso não é problema; em testes redundância é uma boa coisa.

Boas práticas para codificação de testes:
um 'TestClass' separado para cada model ou view
um método de teste separado para cada conjunto de condições que deseja testar
nomes de métodos de teste que descrevem a sua função

Mais testes: 
Há muito mais do que você pode fazer, e uma série de ferramentas muito úteis à disposição para conseguir algumas coisas inteligentes.

Por exemplo, enquanto nossos testes aqui tem coberto algumas lógicas internas de um model e a informação publicada nas views, você pode usar um framework para browsers como o Selenium para testar a maneira como seu HTML é processado de verdade pelo browser. Isso permite testar não somente o comportamento do código Django, mas também, por exemplo, do código javascript. É bem legal ver seus testes iniciarem um navegador, e começar a interagir com seu site, como se fosse um ser humano guiando! 
O Django inclui a LiveServerTestCase para facilitar a integração com ferramentas como o Selenium.
Se você tem uma aplicação complexa, você pode querer rodar testes automaticamente com cada commit pelo propósito de integração contínua, e o próprio controle de qualidade - ao menos parcialmente - é automatizado.
Uma boa forma de encontrar partes não testadas de sua aplicação é verificar a cobertura de código. Isto também ajuda a identificar códigos frágeis ou mesmo morto. Se você não pode testar um pedaço do código, isso normalmente significa que o código precisa ser refatorado ou removido.
--------------------------------------------------------------------

Tutorial 10 de 10

Codificando uma app simples Continuando a alteração no admin... Personalizando a listagem da página de Administração: Vamos fazer melh...