Fala pessoal,
Depois de 1 semana sem conseguir gravar, nesta live focamos em finalizar o endpoint de listagem dos pomodoros de uma determinada tarefa. Segue o vídeo:
Implementação do endpoint
Eu pensei novamente em como eu poderia retornar os dados quando eu solicitar quais pomodoros essa tarefa tem, e decidi mudar a estrutura da resposta:
{ "uid": "baa9f00a-873a-4239-8294-8f3c8b0943d1", "description": "Task name", "pomodoros": { "completed": 1, "uncompleted": 1 } }
Agora dessa forma faz mais sentido porque o objetivo dessas informações para o cliente seria de entender quantos pomodoros foram completos e quantos acabaram terminando antes do tempo adequado de 25 minutos. O pomodoro incompleto nesse momento, pode ser interpretado como interrupções principalmente, então como não salvamos por enquanto o motivo das interrupções, trazer as datas não serviria muito para quem vai consumir, fora que vai trafegar mais dados para pouco resultado.
Refatorando a implementação da rota
Antes a view referente a essa rota retornava somente um dado fake para fazer o teste passar, já que acabamos iniciando a implementação por ele. Mas precisei deixar ele de lado para finalizar o endpoint de criação de pomodoros na última live porque assim eu teria a estrutura que realmente funcionava, e foi o que fiz. Agora temos insumos para refatorar e implementar o código de produção. Começando pelo teste do serializer:
@pytest.mark.django_db def test_list_pomodoro_serializer(add_task, add_pomodoro): task = add_task(description="Task Pomodoro") add_pomodoro(task=task, completed=True) data = { "uid": str(task.uid), "description": task.description, "pomodoros": {"completed": 1, "incompleted": 0}, } serializer = TaskPomodorosSerializer(task) assert json.dumps(serializer.data) == json.dumps(data)
O teste acima valida a estrutura em que o serializer vai retornar. Criei um novo serializer para que ele prepare a estrutura da resposta do endpoint. Segue o seu código abaixo:
class PomodorosInfoSerializer(serializers.ModelSerializer): completed = serializers.SerializerMethodField() incompleted = serializers.SerializerMethodField() class Meta: model = Task fields = ["completed", "incompleted"] def get_completed(self, obj): return obj.filter(completed=True).count() def get_incompleted(self, obj): return obj.filter(completed=False).count() class TaskPomodorosSerializer(serializers.ModelSerializer): pomodoros = PomodorosInfoSerializer() class Meta: model = Task fields = ["uid", "description", "pomodoros"] read_only_fields = ["uid", "description" "pomodoros"]
Começando de baixo para cima:
- Nas linhas 16 a 22 temos o serializer que será usado na view de listar pomodoros. Ele é um ModelSerializer de Task para que possamos acessar sua instância e retornar alguns dados da tarefa como o uid e description. Além disso ele possui um field pomodoros em que é um NestedSerializer que é importante para compor a estrutura
- Nas linhas 1 a 13 é o serializer responsável por retornar aqueles totalizadores dos pomodoros (completos/incompletos). Ele tem duas propriedades que são um SerializerMethodField que nada mais é do que um campo que executa e retorna o resultado de uma função que por padrão tem o nome get_<property>, e que como argumento passa o objeto referente ao modelo. Através do obj eu faço a consulta referente aos tipos de pomodoros e retorno o total.
Agora tem alguns pontos importantes a esclarecer sobre esse Serializer:
O PomodorosInfoSerializer está sendo usado como um NestedSerializer e como ele possui o nome pomodoros, o Django REST é inteligente para definir o parâmetro obj como um Related Manager. Como assim? Vamos olhar novamente o models.py
# ... class Pomodoro(models.Model): task = models.ForeignKey( "core.Task", related_name="pomodoros", on_delete=models.CASCADE ) started_at = models.DateTimeField() # ...
Na linha 4 definimos a relação do pomodoro com a tarefa, mas também nomeamos o related_name para pomodoros. Dessa forma o Django REST definiu o parâmetro obj como justamente esse manager. Dessa forma conseguimos fazer aquele filtro nos métodos get_completed e get_incompleted.
Com isso pronto, refatorei o restante do código como a view e os testes:
# ... @pytest.mark.django_db def test_request_task_pomodoros(client, add_task): task = add_task(description="Task Pomodoro") resp = client.get(f"/api/tasks/{task.uid}/pomodoros") assert resp.status_code == 200 @pytest.mark.django_db def test_response_task_pomodoros(client, add_task, add_pomodoro): task = add_task(description="Task Pomodoro") add_pomodoro(task=task, completed=True) resp = client.get(f"/api/tasks/{task.uid}/pomodoros") total_completed = task.pomodoros.filter(completed=True).count() total_incompleted = task.pomodoros.filter(completed=False).count() assert len(resp.data) > 0 assert resp.data["uid"] == str(task.uid) assert resp.data["description"] == task.description assert resp.data["pomodoros"]["completed"] == total_completed assert resp.data["pomodoros"]["incompleted"] == total_incompleted #...
class TaskPomodoroAPIView(APIView): def get(self, request, uid): task = get_object_or_404(Task, uid=uid) serializer = TaskPomodorosSerializer(task) return Response(serializer.data) def post(self, request, uid): # ...
Criando um Custom Manager
Apesar da consulta ser simples, sempre que possível é recomendado isolar certos detalhes de implementação de negócio relativos a consultas em um Manager customizado. Abaixo está o teste e o código:
@pytest.mark.django_db def test_count_completed_pomodoros(add_task, add_pomodoro): task = add_task(description="Task Pomodoro") add_pomodoro(task=task, completed=True) assert task.pomodoros.total_completed() == 1 @pytest.mark.django_db def test_count_incompleted_pomodoros(add_task, add_pomodoro): task = add_task(description="Task Pomodoro") add_pomodoro(task=task, completed=False) assert task.pomodoros.total_incompleted() == 1
from django.db import models class PomodoroManager(models.Manager): def total_completed(self): return super().get_queryset().filter(completed=True).count() def total_incompleted(self): return super().get_queryset().filter(completed=False).count()
Dessa forma fazendo a consulta usando total_completed e total_incompleted fica mais semântico, e quaisquer futuras mudanças na consulta, vai refletir automaticamente em quem consome esses métodos. Agora é refatorar o serializer:
class PomodorosInfoSerializer(serializers.ModelSerializer): completed = serializers.SerializerMethodField() incompleted = serializers.SerializerMethodField() class Meta: model = Task fields = ["completed", "incompleted"] def get_completed(self, obj): return obj.total_completed() def get_incompleted(self, obj): return obj.total_incompleted()
Com isso feito, temos nossos recursos de Pomodoro implementados e testados
O que vem agora?
Com os endpoint implementados agora vou resolver alguns detalhes que deixei passar para deixar o projeto e o seu fluxo mais coerente, como por exemplo, implementar a documentação da API usando o drf-yasg. Finalizado esses detalhes, podemos partir para o frontend.
Até a próxima live pessoal!