# 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`.

{% code lineNumbers="true" %}

```python
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_) #
		
```

{% endcode %}

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.

```
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....FF..
======================================================================
FAIL: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
    self.assertEqual(
AssertionError: 404 != 200 : Couldn't retrieve content: Response code was 404 (expected 200)

======================================================================
FAIL: test_uses_list_template (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: No templates used to render the response

----------------------------------------------------------------------
Ran 8 tests in 0.019s

FAILED (failures=2)
Destroying test database for alias 'default'...
```

#### 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.

```
from django.urls import path, re_path
from lists import views

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/new', views.new_list, name='new_list'),
    re_path(r'^lists/(.+)/$', views.view_list, name='view_list'),
]
```

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

```
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....EE.E
======================================================================
ERROR: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
TypeError: view_list() takes 1 positional argument but 2 were given

======================================================================
ERROR: test_uses_list_template (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
TypeError: view_list() takes 1 positional argument but 2 were given

======================================================================
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
TypeError: view_list() takes 1 positional argument but 2 were given

----------------------------------------------------------------------
Ran 8 tests in 0.035s

FAILED (errors=3)
Destroying test database for alias 'default'...
```

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:

{% code lineNumbers="true" %}

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

# Create your views here.
def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	items = Item.objects.all()
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect('/lists/the-only-list-in-the-world/')
```

{% endcode %}

Ao executar os testes agora nosso erro muda.

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F...
======================================================================
FAIL: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 1 != 0 : Response should not contain 'other list item 1'

----------------------------------------------------------------------
Ran 8 tests in 0.018s

FAILED (failures=1)
Destroying test database for alias 'default'...
```

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:

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

# Create your views here.
def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect('/lists/the-only-list-in-the-world/')
```

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......E
======================================================================
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/fields/__init__.py", line 1823, in get_prep_value
    return int(value)
ValueError: invalid literal for int() with base 10: 'the-only-list-in-the-world'

The above exception was the direct cause of the following exception:
...
ValueError: Field 'id' expected a number but got 'the-only-list-in-the-world'.

----------------------------------------------------------------------
Ran 8 tests in 0.035s

FAILED (errors=1)
Destroying test database for alias 'default'...
```

#### 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:

{% code lineNumbers="true" %}

```python
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'})
		new_list = List.objects.first()
		self.assertRedirects(response, f'/lists/{new_list.id}/')


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_)
```

{% endcode %}

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......E
======================================================================
ERROR: test_redirects_after_POST (lists.tests.NewListTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/db/models/fields/__init__.py", line 1823, in get_prep_value
    return int(value)
ValueError: invalid literal for int() with base 10: 'the-only-list-in-the-world'

The above exception was the direct cause of the following exception:
...
ValueError: Field 'id' expected a number but got 'the-only-list-in-the-world'.

----------------------------------------------------------------------
Ran 8 tests in 0.034s

FAILED (errors=1)
Destroying test database for alias 'default'...
```

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

{% code lineNumbers="true" %}

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

# Create your views here.
def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')
```

{% endcode %}

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

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.021s

OK
Destroying test database for alias 'default'...
```

#### Regressão nos Testes Funcionais

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

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F.
======================================================================
FAIL: test_can_start_a_list_for_one_user (tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: '2: Use peacock feathers to make a fly' not found in ['1: Use peacock feathers to make a fly']

----------------------------------------------------------------------
Ran 2 tests in 18.979s

FAILED (failures=1)
Destroying test database for alias 'default'...
```

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

{% code lineNumbers="true" %}

```python
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'})
		new_list = List.objects.first()
		self.assertRedirects(response, f'/lists/{new_list.id}/')


class NewItemTets(TestCase):
	def test_can_save_a_POST_request_to_an_existing_list(self):
		other_list = List.objects.create()
		correct_list = List.objects.create()

		self.client.post(
			f'/lists/{correct_list.id}/add_item',
			data={'item_text': 'A new item for an existing list'}
		)

		self.assertEqual(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEqual(new_item.text, 'A new item for an existing list')
		self.assertEqual(new_item.list, correct_list)

	def test_redirects_to_list_view(self):
		other_list = List.objects.create()
		correct_list = List.objects.create()

		response = self.client.post(
			f'/lists/{correct_list.id}/add_item',
			data={'item_text': 'A new item for an existing list'}
		)

		self.assertRedirects(response, f'/lists/{correct_list.id}/')


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_)
```

{% endcode %}

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......FF..
======================================================================
FAIL: test_can_save_a_POST_request_to_an_existing_list (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/lists/tests.py", line 45, in test_can_save_a_POST_request_to_an_existing_list
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1

======================================================================
FAIL: test_redirects_to_list_view (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 301 != 302 : Response didn't redirect as expected: 
Response code was 301 (expected 302)

----------------------------------------------------------------------
Ran 10 tests in 0.022s

FAILED (failures=2)
Destroying test database for alias 'default'...
```

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.

{% code lineNumbers="true" %}

```python
from django.urls import path, re_path
from lists import views

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/new', views.new_list, name='new_list'),
    re_path(r'^lists/(\d+)/$', views.view_list, name='view_list'),
]
```

{% endcode %}

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......FF..
======================================================================
FAIL: test_can_save_a_POST_request_to_an_existing_list (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 0 != 1

======================================================================
FAIL: test_redirects_to_list_view (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
AssertionError: 404 != 302 : Response didn't redirect as expected: Response code was 404 (expected 302)

----------------------------------------------------------------------
Ran 10 tests in 0.023s

FAILED (failures=2)
Destroying test database for alias 'default'...
```

#### &#x20;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:

{% code lineNumbers="true" %}

```python
from django.urls import path, re_path
from lists import views

urlpatterns = [
    path('', views.home_page, name='home'),
    path('lists/new', views.new_list, name='new_list'),
    re_path(r'^lists/(\d+)/$', views.view_list, name='view_list'),
    re_path(r'^lists/(\d+)/add_item$', views.add_item, name='add_item'),
]
```

{% endcode %}

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.

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
Destroying test database for alias 'default'...
Traceback (most recent call last):
...
AttributeError: module 'lists.views' has no attribute 'add_item'
```

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

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')

def add_item(request):
	pass
```

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.

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......EE..
======================================================================
ERROR: test_can_save_a_POST_request_to_an_existing_list (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
TypeError: add_item() takes 1 positional argument but 2 were given

======================================================================
ERROR: test_redirects_to_list_view (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
TypeError: add_item() takes 1 positional argument but 2 were given

----------------------------------------------------------------------
Ran 10 tests in 0.036s

FAILED (errors=2)
Destroying test database for alias 'default'...
```

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:

{% code lineNumbers="true" %}

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')

def add_item(request, list_id):
	pass
```

{% endcode %}

Agora nossos testes apresentam o erro abaixo:

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......EE..
======================================================================
ERROR: test_can_save_a_POST_request_to_an_existing_list (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
ValueError: The view lists.views.add_item didn't 
return an HttpResponse object. It returned None instead.

======================================================================
ERROR: test_redirects_to_list_view (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
...
ValueError: The view lists.views.add_item didn't 
return an HttpResponse object. It returned None instead.

----------------------------------------------------------------------
Ran 10 tests in 0.039s

FAILED (errors=2)
Destroying test database for alias 'default'...
```

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

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')

def add_item(request, list_id):
	list_ = List.objects.get(id=list_id)
	return redirect(f'/lists/{list_.id}/')
```

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......F...
======================================================================
FAIL: test_can_save_a_POST_request_to_an_existing_list (lists.tests.NewItemTets)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mlptdd/superlists/superlists/lists/tests.py", line 45, in test_can_save_a_POST_request_to_an_existing_list
    self.assertEqual(Item.objects.count(), 1)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 10 tests in 0.027s

FAILED (failures=1)
Destroying test database for alias 'default'...
```

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:

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	items = Item.objects.filter(list=list_)
	return render(request, 'list.html', {'items': items})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')

def add_item(request, list_id):
	list_ = List.objects.get(id=list_id)
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')
```

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..........
----------------------------------------------------------------------
Ran 10 tests in 0.025s

OK
Destroying test database for alias 'default'...
```

#### 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.

```markup
<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<form method="POST" action="/lists/{{list.id}}/add_item">
			<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
			{% csrf_token %}
		</form>
		<table id="id_list_table">
			{% for item in items %}
			<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
			{% endfor %}
		</table>
	</body>
</html>
```

{% code lineNumbers="true" %}

```python
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'})
		new_list = List.objects.first()
		self.assertRedirects(response, f'/lists/{new_list.id}/')


class NewItemTets(TestCase):
	def test_can_save_a_POST_request_to_an_existing_list(self):
		other_list = List.objects.create()
		correct_list = List.objects.create()

		self.client.post(
			f'/lists/{correct_list.id}/add_item',
			data={'item_text': 'A new item for an existing list'}
		)

		self.assertEqual(Item.objects.count(), 1)
		new_item = Item.objects.first()
		self.assertEqual(new_item.text, 'A new item for an existing list')
		self.assertEqual(new_item.list, correct_list)

	def test_redirects_to_list_view(self):
		other_list = List.objects.create()
		correct_list = List.objects.create()

		response = self.client.post(
			f'/lists/{correct_list.id}/add_item',
			data={'item_text': 'A new item for an existing list'}
		)

		self.assertRedirects(response, f'/lists/{correct_list.id}/')


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

	def test_passes_correct_list_to_template(self):
		other_list = List.objects.create()
		correct_list = List.objects.create()
		response = self.client.get(f'/lists/{correct_list.id}/')
		self.assertEqual(response.context['list'], correct_list)


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_)
```

{% endcode %}

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:

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....E.....
======================================================================
ERROR: test_passes_correct_list_to_template (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
...
KeyError: 'list'

----------------------------------------------------------------------
Ran 11 tests in 0.029s

FAILED (errors=1)
Destroying test database for alias 'default'...
```

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:

{% code lineNumbers="true" %}

```python
from django.shortcuts import redirect, render
from lists.models import Item, List

def home_page(request):
	return render(request, 'home.html')

def view_list(request, list_id):
	list_ = List.objects.get(id=list_id)
	return render(request, 'list.html', {'list': list_})

def new_list(request):
	list_ = List.objects.create()
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')

def add_item(request, list_id):
	list_ = List.objects.get(id=list_id)
	Item.objects.create(text=request.POST['item_text'], list=list_)
	return redirect(f'/lists/{list_.id}/')
```

{% endcode %}

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`.

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F......
======================================================================
FAIL: test_displays_only_items_for_that_list (lists.tests.ListViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/mltdd/superlists/superlists/lists/tests.py", line 81, in test_displays_only_items_for_that_list
    self.assertContains(response, 'itemey 1')
  File "/home/mlptdd/superlists/lib/python3.8/site-packages/django/test/testcases.py", line 471, in assertContains
    self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s in response" % text_repr)
AssertionError: False is not true : Couldn't find 'itemey 1' in response

----------------------------------------------------------------------
Ran 11 tests in 0.030s

FAILED (failures=1)
Destroying test database for alias 'default'...
```

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.&#x20;

```markup
<html>
	<head>
		<title>To-Do lists</title>
	</head>
	<body>
		<h1>Your To-Do list</h1>
		<form method="POST" action="/lists/{{list.id}}/add_item">
			<input name="item_text" id="id_new_item" placeholder="Enter a to-do item" />
			{% csrf_token %}
		</form>
		<table id="id_list_table">
			{% for item in list.item_set.all %}
			<tr><td>{{ forloop.counter }}: {{ item.text }}</td></tr>
			{% endfor %}
		</table>
	</body>
</html>
```

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.

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 0.028s

OK
Destroying test database for alias 'default'...
```

E nossos testes funcionais?

```bash
(superlists) auri@av:~/superlists/superlists$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 9.392s

OK
Destroying test database for alias 'default'...
```

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

```bash
(superlists) auri@av:~/superlists/superlists$ git status
No ramo master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (utilize "git add <arquivo>..." para atualizar o que será submetido)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   functional_tests/tests.py
	modified:   lists/templates/list.html
	modified:   lists/tests.py
	modified:   lists/views.py
	modified:   superlists/urls.py

nenhuma modificação adicionada à submissão (utilize "git add" e/ou "git commit -a")

(superlists) auri@av:~/superlists/superlists$ git commit -am "New URL + view for adding to existing list. FT passes :-)"
[master 9152a2e] New URL + view for adding to existing list. FT passes :-)
 5 files changed, 72 insertions(+), 27 deletions(-)

(superlists) auri@av:~/superlists/superlists$ git push
Username for 'https://github.com': aurimrv
Password for 'https://aurimrv@github.com': 
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 12 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (11/11), 1.54 KiB | 1.54 MiB/s, done.
Total 11 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), completed with 8 local objects.
To https://github.com/aurimrv/superlists.git
   20f8359..9152a2e  master -> master
```

#### Ú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.&#x20;

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.

```bash
(superlists) auri@av:~/superlists/superlists$ cp superlists/urls.py lists/urls.py
```

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

```python
from django.conf.urls import include
from django.urls import re_path
from lists import views as list_views
from lists import urls as list_urls

urlpatterns = [
    re_path(r'^$', list_views.home_page, name='home'),
    re_path(r'^lists/', include(list_urls)),
]
```

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

```python
from django.urls import re_path
from lists import views

urlpatterns = [
    re_path(r'^new$', views.new_list, name='new_list'),
    re_path(r'^(\d+)/$', views.view_list, name='view_list'),
    re_path(r'^(\d+)/add_item$', views.add_item, name='add_item'),
]
```

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test lists
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 0.028s

OK
Destroying test database for alias 'default'...
```

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

```python
(superlists) auri@av:~/superlists/superlists$ python manage.py test functional_tests
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 10.334s

OK
Destroying test database for alias 'default'...
```

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

```python
(superlists) auri@av:~/superlists/superlists$ git status
No ramo master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (utilize "git add <arquivo>..." para atualizar o que será submetido)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   superlists/urls.py

Arquivos não monitorados:
  (utilize "git add <arquivo>..." para incluir o que será submetido)
	lists/urls.py

nenhuma modificação adicionada à submissão (utilize "git add" e/ou "git commit -a")

(superlists) auri@av:~/superlists/superlists$ git add lists/urls.py 

(superlists) auri@av:~/superlists/superlists$ git add superlists/urls.py 

(superlists) auri@av:~/superlists/superlists$ git commit -am "URL include successful. Unit and Functional test pass."
[master 0f1bd85] URL include successful. Unit and Functional test pass.
 2 files changed, 29 insertions(+), 6 deletions(-)
 create mode 100644 lists/urls.py

(superlists) auri@av:~/superlists/superlists$ git push
Username for 'https://github.com': aurimrv
Password for 'https://aurimrv@github.com': 
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 12 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 674 bytes | 674.00 KiB/s, done.
Total 6 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 4 local objects.
To https://github.com/aurimrv/superlists.git
   9152a2e..0f1bd85  master -> master
```

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.

```bash
(superlists) tdd@mlp:~/superlists/superlists$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, lists, sessions
Running migrations:
  Applying lists.0003_list... OK
  Applying lists.0004_item_list... OK
  Applying lists.0005_alter_item_list... OK

```

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