Fala pessoal,
Nessa live continuamos na implementação dos endpoints relacionados a criação e consulta de pomodoros completos e incompletos. No meio do caminho me deparei sobre a decisão de usar uma rota para criação de pomodoro para ser o mais próximo possível das boas prática em um app RESTFull, como o nested resources . Foi interessante lidar com isso na live porque mostra algumas limitações do Django REST Framework, e como ele pode amarrar você em relação a algumas decisões de design. Segue o vídeo para entender melhor todo o processo:
Implementando a criação de pomodoros
No Pull Request você consegue ver com mais detalhes as mudanças feitas, mas vamos pontuar algumas evoluções importantes. Na última live parei nos testes e na implementação do endpoint de criação de pomodoros (mas que ainda não salvava nada e retornada dados fake), e no serializer responsável por lidar com recebimento dos dados e sua serialização para a resposta. Agora retomamos isso implementando os testes e o modelo de Pomodoro, para refatorarmos as camadas superiores e assim finalizar essa funcionalidade.
@pytest.mark.django_db def test_task_pomodoro_model(add_task, add_pomodoro): task = add_task(description="Task Pomodoro") pomodoro = add_pomodoro(task=task, completed=True) assert pomodoro.id assert pomodoro.task.id == task.id assert pomodoro.completed
Como podem ver eu fiz uso do pytest fixtures para que criasse uma task e depois um pomodoro completo (com mais pelo menos 25 minutos de duração). Segue o trecho do arquivo conftest.py.
# ... @pytest.fixture(scope="function") def add_pomodoro(): def _add_pomodoro(task, completed=True): started_at = timezone.now() ended_at = timezone.now() + timedelta(minutes=POMODORO_UNIT_TIMER) pomodoro = Pomodoro.objects.create( task=task, started_at=started_at, ended_at=ended_at ) return pomodoro return _add_pomodoro
Fiz o uso do django.utils para importar o timezone e gerar uma data, respeitando a propriedade USE_TZ do projeto. Além disso movi a constante POMODORO_UNIT_TIMER para o modelo e fiz uso para gerar um intervalo de tempo de exatos 25 minutos. Esse código vou refatorar porque o parâmetro completed não fez muito sentido aqui, porque caso o programador escolher um pomodoro incompleto, não vai retornar corretamente porque não implementei isso (estava focado em finalizar pelo menos esse recurso na API na live 😂). Agora segue o modelo:
# ... POMODORO_UNIT_TIMER = 25 ONE_MINUTE = 60 # ... class Pomodoro(models.Model): task = models.ForeignKey( "core.Task", related_name="pomodoros", on_delete=models.CASCADE ) started_at = models.DateTimeField() ended_at = models.DateTimeField() completed = models.BooleanField(default=False) def clean(self) -> None: diff = self.ended_at - self.started_at total_minutes = diff.seconds / ONE_MINUTE if total_minutes >= POMODORO_UNIT_TIMER: self.completed = True def save(self, *args, **kwargs) -> None: self.clean() return super().save(*args, **kwargs)
Aqui vou explicar em partes:
- Nas linhas 3-4 eu movi aquelas constantes porque eles seriam usados de fato no modelo e não mais no serializer
- Na linha 8 em diante está o modelo de pomodoro em que vai armazenar a data de inicio e fim do pomodoro, a relação com a tarefa que é importante, além de uma flag para garantir se o pomodoro foi completo ou não
- Nas linhas 16-21 implementei o método clean, para validar os dados do modelo, movendo toda aquela regra de pomodoro que estava no serializer. Tomei essa decisão porque faz mais sentido o modelo ter isso internamente do que o serializer, já que, por exemplo, se o modelo for utilizado em outros lugares e criassem um pomodoro através dela, essa regra vai ser garantida.
- E nas linhas 23-25 sobrescrevi o método save somente para garantir que o método clean será executado.
Com o modelo criado e testado, partimos para refatorar as outras partes relacionados ao recurso de novo pomodoro:
class TaskPomodoroSerializer(serializers.ModelSerializer): class Meta: model = Pomodoro fields = ("id", "started_at", "ended_at", "completed") def __init__(self, task, **kwargs): self.task = task super().__init__(**kwargs) def create(self, validated_data): return Pomodoro.objects.create(task=self.task, **validated_data)
class TaskPomodoroAPIView(APIView): # def get(...) def post(self, request, uid): task = get_object_or_404(Task, uid=uid) serializer = TaskPomodoroSerializer(task=task, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED)
Olhando esse código pode-se perguntar: porque alterei o __init__ do serializer? Fiz isso por conta da decisão de design da rota que falei no início. Para que um pomodoro seja criado ele precisa usar a seguinte a rota POST /api/tasks/{uid}/pomodoros. Tentando fazer uma leitura dessa rota a idéia é:
Crie um novo pomodoro para a tarefa XYZ
Que é aonde entra no que falei sobre Nested Resources. O Django REST nesse momento acabou me limitando nessa questão, tanto que existem alguns apps focados em ajudar a resolver isso como o drf-extensions por exemplo. O problema é que eles oferecem a solução em cima de ViewSets e como a idéia é construir parte por parte via TDD, eu tomei a decisão em usar o APIView da biblioteca. Dessa forma, eu teria um impacto considerável em mudar para ViewSets para somente suportar o nested routes da dependência externa. Portanto fiz o seguinte:
- Sobrescrevi o __init__ do TaskPomodoroSerializer para receber a instância de task (nas linhas 6-8 do serializers.py) buscada na view por meio do get_object_or_404 do Django (na linha 5 do views.py)
- Atribuo essa instância a uma propriedade na classe (self.task) para ser utilizado posteriormente (na linha 7 do serializers.py).
- Quando a view invoca o serializer.save() ele vai invocar o método create() que também foi sobrescrito para associar a tarefa ao modelo de Pomodoro (nas linhas 10-11 do serializers.py)
Feito dessa forma, o impacto foi pequeno e não precisei de uma nova dependência. Mas por outro lado mostra que para uma abordagem como essa que não é complexa, precisei “sair um pouco dos trilhos” do Django REST. Ele ajuda a implementar APIs de forma rápida, mas quando você começa a precisar ter mais autonomia no design da sua aplicação, muita das vezes vai ter que passar dos limites do Convention over Configuration da biblioteca.
Quem quiser acompanhar a evolução do codebase é só acessar o repositório e visualizar com mais detalhes a implementação na live.
Até a próxima!