Live Coding: Tomatex

Codando e Aprendendo: Criando pomodoro timer com Django + React #6

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!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *