6.7 URL Próprio para Cada Lista

Conforme nossa história, cada usuário deve ver sua lista em um URL próprio e único. Podemos pensar em inúmeras maneiras de gerar URLs únicos. Nossa primeira solução será utilizar o próprio identificador de uma lista em nosso banco de dados, ou seja, o id de cada lista. Como o id é utilizado como chave primária no banco de dados, temos a garantia de que será único para cada lista.

Iniciamos alterando nossos testes unitários (classe ListViewTest - linhas de 34 a 56) para que os testes apontem para novas URLs conforme abaixo. Também alteraremos o nome do método de teste test_displays_all_items para test_displays_only_items_for_that_list.

from django.urls import resolve
from django.test import TestCase
from lists.views import home_page
from lists.models import Item, List #

class HomePageTest(TestCase):

	def test_root_url_resolves_to_home_page_view(self):
		found = resolve('/')
		self.assertEquals(found.func, home_page)

	def test_home_page_returns_correct_html(self):
		response = self.client.get('/')
		self.assertTemplateUsed(response, 'home.html')

	def test_only_saves_items_when_necessary(self):
		self.client.get('/')
		self.assertEquals(Item.objects.count(), 0)


class NewListTest(TestCase):

	def test_can_save_a_POST_request(self):
		self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertEquals(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEquals(new_item.text, 'A new list item')

	def test_redirects_after_POST(self):
		response = self.client.post('/lists/new', data={'item_text': 'A new list item'})
		self.assertRedirects(response, '/lists/the-only-list-in-the-world/')


class ListViewTest(TestCase):

	def test_uses_list_template(self):
		list_ = List.objects.create()
		response = self.client.get(f'/lists/{list_.id}/')
		self.assertTemplateUsed(response, 'list.html')


	def test_displays_only_items_for_that_list(self):
		correct_list = List.objects.create()
		Item.objects.create(text='itemey 1', list=correct_list)
		Item.objects.create(text='itemey 2', list=correct_list)

		other_list = List.objects.create()
		Item.objects.create(text='other list item 1', list=other_list)
		Item.objects.create(text='other list item 2', list=other_list)

		response = self.client.get(f'/lists/{correct_list.id}/')

		self.assertContains(response, 'itemey 1')
		self.assertContains(response, 'itemey 2')
		self.assertNotContains(response, 'other list item 1')
		self.assertNotContains(response, 'other list item 2')


class ListAndItemModelTest(TestCase): #

	def test_saving_and_retriving_items(self):
		list_ = List() #
		list_.save() #

		first_item = Item()
		first_item.text = 'The first (ever) list item'
		first_item.list = list_ #
		first_item.save()

		second_item = Item()
		second_item.text = 'Item the second'
		second_item.list = list_
		second_item.save()

		saved_list = List.objects.first() #
		self.assertEquals(saved_list, list_) #

		saved_items = Item.objects.all()
		self.assertEquals(saved_items.count(),2)

		first_saved_item = saved_items[0]
		second_saved_item = saved_items[1]

		self.assertEquals(first_saved_item.text, 'The first (ever) list item')
		self.assertEquals(first_saved_item.list, list_)	#
		self.assertEquals(second_saved_item.text, 'Item the second')
		self.assertEquals(second_saved_item.list, list_) #
		

Ao executar os testes resulta no 404 que era esperado e outro erro relacionado, visto que o Django ainda não sabe como atender a essas novas URLs.

Capturando Parâmetros de URLs

Observe que precisamos de URLs parametrizadas agora e, para isso, precisamos aprender a passar parâmetros para nossas views. Alteramos o nosso arquivo superlists/urls.py para que o mesmo trate as URLs com id varável. Usamos para isso expressões regulares.

Ao executar os testes após essa alteração o resultado é conforme mostrado abaixo:

A queixa que nossa função de view (view_list) agora precisa receber mais um argumento, exatamente o id de uma lista para poder apresentar seu conteúdo. A forma mais simples de corrigir isso é incluindo um parâmetro fictício na função da view (linha 8), conforme abaixo:

Ao executar os testes agora nosso erro muda.

Isso porque nossa view não está diferenciando para qual lista ela manda cada item e, desse modo, tudo está presente em uma única lista, ocasionando o erro. Para corrigir, vamos novamente alterar o código de lists/views.py, conforme abaixo:

Com isso, agora passamos a ter erros em outros testes conforme abaixo:

Adaptando new_list para lidar com URLs parametrizadas

O erro apresentado acima indica que o teste unitário test_redirects_after_POST ainda não foi adaptado para a nossa nova realizada. Desse modo, o mesmo precisa ser alterado conforme apresentado nas linhas 29 a 32 do código abaixo:

Mesmo com a correção acima, continuamos obtendo o erro de literal inválido como antes.

Nesse caso, precisamos também corrigir nossa view (lists/views.py) e atualizar os redirecionamentos para os locais corretos, conforme abaixo (linha 16).

Isso faz com que nossos testes unitários passem e fiquem novamente consistentes com a implementação.

Regressão nos Testes Funcionais

Apesar do sucesso na execução dos testes unitários, os testes funcionais mostram que tivemos uma regressão.

Os testes funcionais indicam que nossa solução preliminar ainda não resolve o prolema de forma adequada. Como criamos uma lista nova a cada item, não é possível adicionar mais de um item na mesma lista. Tal requisito está associado ao último item da nossa lista de tarefas.

View adicional para inclusão de item em lista existente

Para a inclusão de um item em uma lista existente, precisamos de um novo URL e de uma nova view que trate essa nova URL.

A URL em questão pode ser algo do tipo /lists/<list_id>/add_item. O primeiro passo é a criação de casos de teste unitários para lidar com a inclusão de itens em uma lista existente. Para isso, criamos a classe de teste NewItemTest (linhas 35 a 59).

Ao executar os testes unitários, temos a seguinte saída.

A falha decorrente do teste indica que as URLs não são resolvidas corretamente. Na verdade, o que ocorre é que nossa expressão regular está capturando muito mais URLs do que desejamos. A explicação para isso é que o Django considera que um URL é identificado por uma expressão regular se o mesmo diferir apenas por uma barra '/' que é o que está ocorrendo nesse caso, ou seja, lists/(.+)/está capturando também nossa URL lists/1/add_item, por exemplo.

A solução para isso é sermos mais específicos em nossas expressões regulares para a solução de URLs. Por exemplo, se alterarmos superlists/urls.y conforme abaixo, a URL para adição de item deixa de ser identificada por essa regra. O \d+ na linha 7 faz com que apenas dígitos (um ou mais) passem a ser reconhecidos.

Com essa alteração, o resultado dos testes unitários passa a ser conforme abaixo:

Novo URL para adicionar item

Para resolver agora o problema do 404 acima precisamos incluir nossa nova URL para adição de novo item no arquivo superlists/urls.py, conforme abaixo:

Após a alteração, observamos que existem três URLs bastante semelhantes e, posteriormente, pode ser que queiramos refatorar esse arquivo para melhorar isso. Por hora, vamos apenas executar os testes para ver se avançamos.

Perfeito, URL resolvida, mas função de tratamento na view ainda não implementada. Vamos implementa-la em lists/views.py.

Com a reexecução dos testes observamos que a função foi reconhecida, mas não recebe ainda a quantidade suficiente de parâmetros prevista em nossos testes.

Vamos então atualizar o código da view novamente e colocar mais um argumento (list_id - linha 17) na função add_item, conforme abaixo:

Agora nossos testes apresentam o erro abaixo:

Ou seja, nossa função de view add_item ainda não retorna um objeto do tipo HttpResponse. Vamos corrigir isso conforme abaixo:

Agora nosso erro nos testes unitários passou a ser o exibido abaixo:

Vamos então incluir o item solicitado na lista para que o assert acima passe a funcionar. Novamente vamos então alterar nossa função add_item conforme abaixo:

E com essa alteração nossos testes unitários passam com sucesso.

Testando objetos de contextos de respostas diretamente

Agora que temos nosso URL e uma view para a adição de items na lista, precisamos apenas fazer nosso template funcionar para exibir a lista corretamente. Com isso, vamos então alterar o nosso arquivo de template list.html, atualizando o atributo action conforme linha 7 abaixo, e o criar um caso de teste na classe ListViewTest para nos assegurar de que isso está funcionando corretamente (linhas 86 a 90) dos testes unitários.

Nos testes acima, na linha 90 usamos response.context['list']. Isso representa o contexto que passamos para nossa função de renderização. O Django Test Client o insere no objeto response para nós visando a facilitar nossos testes.

Ao executar os testes unitários acima temos a seguinte saída:

O erro ocorre pois, apesar de estarmos solicitando essa chave no nosso caso de teste, ela ainda não está em nosso objeto response. Temos passá-la no processamento da nossa função de view (linha 9), conforme abaixo:

Ao fazer tal alteração e reexecutar os testes, obtemos o erro abaixo pois, um de nossos testes antigos que usava a chave item que foi substituída por list.

A correção para esse problema pode ser feita no nosso template list.html, ajustando o for que faz a iteração sobre os items de uma lista.

No nosso template acima usamos .item_set, conhecido como lookup reverso e se mostra muito útil aqui. Esse recurso possibilita recuperarmos items relacionados a um objeto a partir de uma tabela diferente. Mais informações sobre o recurso estão disponíveis na documentação oficial do Django em https://docs.djangoproject.com/en/3.2/topics/db/queries/#following-relationships-backward.

Ao finalizar a alteração nossos testes unitários passam com sucesso.

E nossos testes funcionais?

Excelente, também estão aprovados com sucesso. Hora de confirmar nossas alterações e colocar tudo sob controle de versão.

Última refatoração

Para encerrar, podemos refatorar nosso código para considerar o recurso de inclusões de URLs. Definitivamente, o arquivo superlists/urls.py deveria ser utilizado para URLs relacionadas com todo o nosso site e não necessariamente com as URLs de uma aplicação particular.

Desse modo, o Django nos orienta a criarmos nosso próprio arquivo lists/urls.py para conter as URLs relacionadas com o nosso projeto particular.

Para realizar essa alteração, vamos primeiro fazer uma cópia do nosso arquivo urls.py atual para o novo que iremos alterar.

Em seguida, alteramos superlists/urls.py conforme abaixo, usando o recurso do Django denominado de inclusões de URLs.

Em seguida, editamos nosso novo lists/urls.py para ficar conforme abaixo:

Ao executar nossos testes de unidade... Sucesso!!!

Ao executar nossos testes de integração... Também sucesso!!!

Hora para mais um commit de confirmação de nossa refatoração bem sucedida e encerramento de um ciclo completo de TDD.

Finalmente, antes de utilizar o sistema em produção, é necessário executar o comando abaixo para atualizar o esquema do banco de dados de produção.

Feito o migrate, basta abrir a URL http://localhost:8000 e testar o nosso sistema.

Last updated

Was this helpful?