django-drf-tutorial

DJANGO COM DRF (2024)

Tutorial para desenvolvimento de APIs REST usando o Django com DRF (Django Rest Framework). Esse tutorial foi construído a partir do curso em vídeo Django com DRF do Eduardo da Silva.

Este tutorial está em constante desenvolvimento. Envie sugestões e correções para meu email. Se preferir, faça uma solicitação de contribuição ao projeto.


Trilha do Curso

Esse curso é parte de uma trilha de aprendizado. Siga os links abaixo para acessar os outros cursos da trilha:

Bons estudos!


1. Preparação do ambiente

A preparação do ambiente será feita apenas uma vez em cada computador. Ela consiste em instalar e configurar o VS Code, o PDM e o Python.

2. Criação do projeto

2.1 O projeto Livraria

Este projeto consiste em uma API REST para uma livraria. Ele terá as seguintes classes:

Modelo Entidade Relacionamento

O modelo entidade relacionamento (MER) do projeto é o seguinte:

Modelo ER

Diagrama de Classes

O diagrama de classes do projeto é o seguinte:

Diagrama de Classes

Modelo de Dados do Django

O modelo de dados do Django é o seguinte:

Modelo de Dados do Django

2.2 Criação do projeto a partir de um template

IMPORTANTE: Vamos criar o projeto livraria a partir de um repositório de template. Se você quiser criar aprender a criar um projeto do zero, acesse o tutorial de 2023.

Feito isso, o repositório livraria será criado no seu GitHub.

2.3 Clonando o projeto

Você pode clonar o projeto de duas formas:

2.3.1 Usando o VS Code

2.3.2 Usando o terminal

git clone <URL do repositório>
code .

O projeto criado ficará assim:

Projeto inicial

2.4 Instalando as dependências

pdm install

2.5 Criando o arquivo .env

Opcionalmente, você pode criar o arquivo .env a partir do terminal, digitando:

cp .env.exemplo .env

2.4 Rodando o servidor de desenvolvimento

pdm run dev

2.5 Acessando o projeto

IMPORTANTE: O servidor de desenvolvimento deve estar sempre rodando para que o projeto funcione.

É isso! Seu projeto está inicializado e rodando!!!

2.6 Exercício

3. Criação de uma aplicação

3.1 Compreendendo uma aplicação

Uma aplicação no Django é um conjunto de arquivos e pastas que contém o código de uma funcionalidade específica do seu site.

Uma aplicação pode ser criada dentro de um projeto ou importada de outro projeto.

Em nosso projeto, temos uma aplicação criada, chamada core, conforme a imagem abaixo:

App core

Todas as aplicações precisam ser adicionadas ao arquivo settings.py do projeto, na seção INSTALLED_APPS.

Dentro da pasta core temos alguns arquivos e pastas, mas os mais importantes são:

O arquivo __init__.py é um arquivo que indica que a pasta é um pacote Python. Ele vai aparecer em todas as pastas que contêm código Python. Muitas vezes, ele é um arquivo vazio.

Posteriormente, iremos modificar esses arquivos, bem como incluir alguns arquivos novos.

3.2 Model User

Um modelo (model) no Django é uma classe que representa uma tabela no banco de dados. Cada atributo (variável) dessa classe representa um campo da tabela.

Para maiores informações consulte a documentação do Django sobre models.

Você pode observar que a pasta models já contém um modelo de dados, dentro do arquivo user.py, chamado User. Esse modelo modifica o usuário padrão fornecido pelo Django e representa um usuário do sistema.

3.3 Criação da model de Categoria

from django.db import models

class Categoria(models.Model):
    descricao = models.CharField(max_length=100)

Nesse código, você:

3.4 Inclusão da model no arquivo __init__.py

from .categoria import Categoria

3.5 Efetivando a criação da tabela

Precisamos ainda efetivar a criação da tabela no banco de dados.

pdm run migrate

Esse comando executará 3 comandos em sequência:

3.6 Inclusão no Admin

A tabela ainda não apareceu, certo? Isso acontece porque ainda não incluímos a model no Admin.

admin.site.register(models.Categoria)

3.7 Exercício

3.8 O campo id

O campo id é criado automaticamente pelo Django. Ele é o identificador único de cada registro da tabela.

3.9 Mudando a forma de exibição dos registros criados

3.10 O método __str__

O método __str__ é um método especial que é chamado quando você tenta imprimir um objeto. Ele é utilizado no Admin e em outros locais para definir como o objeto será exibido.

...
    def __str__(self):
        return self.descricao

Isso fará com que a descrição da categoria seja exibida no lugar de Categoria object (1). O método __str__ é um método especial do Python e deve sempre retornar uma string.

Volte ao Admin verifique o que mudou na apresentação dos objetos da model Categoria.

3.11 Hora de fazer um commit

IMPORTANTE: Escrevendo uma boa mensagem de commit

4. Criação de uma API REST

Nesta aula, vamos criar uma API REST para o projeto livraria. Ao final, teremos uma API completa, que permite criar, listar, atualizar e deletar categorias.

4.1 Instalação e configuração do Django Rest Framework (DRF)

Essas configurações já foram feitas no template que utilizamos para criar o projeto. Se você estiver criando um projeto do zero, terá que fazer essas configurações manualmente.

4.2 Criação do serializer

Um serializer é um objeto que transforma um objeto do banco de dados em um objeto JSON.

from rest_framework.serializers import ModelSerializer

from core.models import Categoria

class CategoriaSerializer(ModelSerializer):
    class Meta:
        model = Categoria
        fields = "__all__"

4.2.1 Explicando o código

4.2.2 Inclusão do serializer no __init__.py

from .categoria import CategoriaSerializer

4.3 Criação da view

Uma view é um objeto que recebe uma requisição HTTP e retorna uma resposta HTTP.

from rest_framework.viewsets import ModelViewSet

from core.models import Categoria
from core.serializers import CategoriaSerializer

class CategoriaViewSet(ModelViewSet):
    queryset = Categoria.objects.all()
    serializer_class = CategoriaSerializer

4.3.1 Explicando o código

4.3.2 Inclusão da view no __init__.py

from .categoria import CategoriaViewSet

4.4 Criação das rotas (urls)

As rotas são responsáveis por mapear as URLs para as views.

...
from core.views import UserViewSet
from core.views import CategoriaViewSet # nova linha

router = DefaultRouter()
router.register(r"categorias", CategoriaViewSet) # nova linha
router.register(r"users", UserViewSet, basename="users")
...

IMPORTANTE: os nomes das rotas serão sempre nomes únicos, no plural e em minúsculas. Nas maiorias das vezes, os colocamos em ordem alfabética.

4.5 Testando a API

Se tudo correu bem, você deve ver a interface do DRF.

Isso deve trazer todas as categorias do banco, no formato JSON.

Nesse caso, 1 é o id do registro no banco de dados.

4.6 Opções de manipulação do banco de dados

As opções disponíveis para manipulação dos dados são:

4.7 Outras ferramentas para testar a API

A interface do DRF é funcional, porém simples e limitada. Algumas opções de ferramentas para o teste da API são:

4.8 Utilizando o Swagger

O Swagger é uma ferramenta que permite a documentação e teste de APIs.

4.9 Exercícios: testando a API e as ferramentas

Instale uma ou mais das ferramentas sugeridas.

4.10 Fazendo um commit

5. Aplicação frontend Vuejs

Agora que temos uma API REST completa, vamos criar uma aplicação frontend em Vuejs para consumir essa API da Categoria.

    npm install
    npm run dev

Se tudo correu bem, execute a aplicação:

Se os dados não aparecerem, entre na opção Inspecionar do seu navegador (F12)

Para maiores detalhes sobre a instalação do npm, acesse o tutorial de Instalação da versão LTS do NodeJS do Prof. Eduardo da Silva.

6. Inclusão da Editora no projeto Livraria

Vamos continuar a criação da API REST para o projeto livraria, criando a model Editora e a API para ela.

6.1 Criação da API para a classe Editora

6.2 Criação e modificação dos arquivos

models/editora.py

from django.db import models

class Editora(models.Model):
    nome = models.CharField(max_length=100)
    site = models.URLField(max_length=200, blank=True, null=True)

    def __str__(self):
        return self.nome

models/__init__.py

...
from .editora import Editora

admin.py

...
admin.site.register(models.Editora)

serializers/editora.py

from rest_framework.serializers import ModelSerializer

from core.models import Categoria, Editora
...
class EditoraSerializer(ModelSerializer):
    class Meta:
        model = Editora
        fields = "__all__"

serializers/__init__.py

...
from .editora import EditoraSerializer

views/editora.py

from rest_framework.viewsets import ModelViewSet

from core.models import Categoria, Editora
from core.serializers import CategoriaSerializer, EditoraSerializer

...
class EditoraViewSet(ModelViewSet):
    queryset = Editora.objects.all()
    serializer_class = EditoraSerializer

views/__init__.py

...
from .editora import EditoraViewSet

urls.py

...
from core.views import UserViewSet, CategoriaViewSet, EditoraViewSet
...
router.register(r"categorias", CategoriaViewSet)
router.register(r"editoras", EditoraViewSet)
...

6.3 Fazendo a migração e efetivando a migração

pdm run migrate

6.4 Exercícios: testando da API da Editora

6.5 Fazendo um commit

7. Criação da API para Autor

Vamos continuar a criação da API REST para o projeto livraria, criando a model Autor e a API para ela. Os passos são os mesmos que fizemos para as classes Categoria e Editora.

O autor terá os seguintes atributos:

Exercícios:

8. Criação da API para Livro

Vamos continuar a criação da API REST para o projeto livraria, criando a model Livro e a API para ela. Os passos iniciais são os mesmos que fizemos para as classes Categoria, Editora e Autor.

8.1 Criação dos arquivos necessários

Utilizando um comando no terminal, é possível criar todos os arquivos necessários para a criação da API para a classe Livro.

touch core/models/livro.py core/serializers/livro.py core/views/livro.py

É possivel também abrir todos os arquivos de uma vez, utilizando o comando:

code core/models/livro.py core/models/__init__.py core/admin.py core/serializers/livro.py core/serializers/__init__.py core/views/livro.py core/views/__init__.py app/urls.py

Se você preferir, pode criar os arquivos utilizando o VS Code, como já fizemos anteriormente.

Você deve estar se perguntando: posso criar um comando para fazer isso automaticamente? Sim, pergunte-me como. :)

8.2 Criando o modelo de dados Livro


class Livro(models.Model):
    titulo = models.CharField(max_length=255)
    isbn = models.CharField(max_length=32, null=True, blank=True)
    quantidade = models.IntegerField(default=0,  null=True, blank=True)
    preco = models.DecimalField(max_digits=7, decimal_places=2, default=0, null=True, blank=True)

    def __str__(self):
        return f"({self.id}) {self.titulo} ({self.quantidade})"

Inclua o modelo no arquivo __init__.py da pasta models:

from .livro import Livro

Seu projeto deve ficar assim:

Projeto com a model Livro

8.3 Criando a API para a classe Livro

Da mesma forma que fizemos para as classes Categoria, Editora e Autor, vamos criar a API para a classe Livro.

Siga os passos conforme já definimos.

9. Inclusão das chaves estrangeiras no modelo Livro

Nosso livro terá uma categoria e uma editora. Para isso, vamos incluir campos que serão chaves estrangeiras, referenciando os modelos Categoria e Editora. Esse relacionamento é do tipo n para 1. Posteriormente, vamos incluir um relacionamento n para n entre Livro e Autor.

9.1 Campo categoria no Livro

from .categoria import Categoria
...
    categoria = models.ForeignKey(
        Categoria, on_delete=models.PROTECT, related_name="livros", null=True, blank=True
    )
...

9.2 Campo editora no Livro

from .editora import Editora
...
editora = models.ForeignKey(Editora, on_delete=models.PROTECT, related_name="livros", null=True, blank=True)

Observe que os campos categoria_id e editora_id foram criados no banco de dados, na tabela core_livro. Eles são os campos que fazem referência às tabelas core_categoria e core_editora.

A model Livro ficará assim:

Projeto com a model Livro

9.3 Testando o atributo on_delete

Feito isso, verifique se tudo funcionou.

No Admin:

9.4 Testando o atributo related_name no Django Shell

No Django Shell (que iremos estudar em mais detalhes em uma aula mais adiante), é possível testar o acesso a todos os livros de uma categoria usando algo parecido com isso:

pdm run shellp
>>> Categoria.objects.get(id=1).livros.all()

O comando pdm run shellp é utilizado para abrir o Django Shell Plus com o ambiente virtual do projeto.

10. Inclusão do relacionamento n para n no modelo do Livro

10.1 Model com ManyToManyField - Livros com vários autores

Um livro pode ter vários autores, e um autor pode escrever vários livros. Sendo assim, criaremos agora um relacionamento n para n entre Livro e Autor. Para isso, utilizaremos um campo do tipo ManyToManyField.

Uma outra forma de fazer isso seria criar uma tabela associativa (o que faremos posteriormente). Isso seria útil se quiséssemos armazenar informações adicionais sobre o relacionamento, como o papel do autor no livro (autor principal, coautor, etc.).

from .autor import Autor
...
autores = models.ManyToManyField(Autor, related_name="livros", blank=True)
...

Observe que o campo autores não foi criado na tabela core_livro. Ao invés disso, uma tabela associativa foi criada, com o nome core_livro_autores, contendo os campos livro_id e autor_id. É assim que é feito um relacionamento n para n no Django.

Nesse caso, não é necessário usar o atributo null=True e blank=True, pois um campo do tipo ManyToManyField cria uma tabela associativa.

Projeto com a model Livro

Note que na ligação entre Livro e Autor existem uma “bolinha” em cada lado, indicando que o relacionamento é n para n.

Já no caso de Livro com Categoria e Editora, existe uma “bolinha” em Livro e um “pino” em Categoria e Editora, indicando que o relacionamento é n para 1.

Observe as alterações no banco de dados, no Admin e na API.

10.2 Exercícios

11. Modificação da API para Livro

Observou que no Livro, aparecem apenas os campos id da categoria, da editora e dos autores e não as descrições?

Criação de múltiplos serializadores

Podemos criar múltiplos serializadores para um mesmo modelo, de forma a apresentar as informações de diferentes formas, dependendo da operação.

Apresentação das informações detalhadas no Livro

Uma forma de mostrar essas informações é essa, em serializers.py:

class LivroSerializer(ModelSerializer):
    class Meta:
        model = Livro
        fields = "__all__"
        depth = 1

Teste e você verá que isso resolve a listagem (GET), mas gera problema na criação e alteração (POST, PUT e PATCH).

class LivroSerializer(ModelSerializer):
    class Meta:
        model = Livro
        fields = "__all__"


class LivroListRetrieveSerializer(ModelSerializer):
    class Meta:
        model = Livro
        fields = "__all__"
        depth = 1
from .livro import LivroListRetrieveSerializer, LivroSerializer

Observe que no LivroListRetrieveSerializer foi incluído o atributo depth = 1, que permite a apresentação dos dados relacionados.

...
from core.serializers import LivroListRetrieveSerializer, LivroSerializer


class LivroViewSet(ModelViewSet):
    queryset = Livro.objects.all()
    serializer_class = LivroSerializer

    def get_serializer_class(self):
        if self.action in ["list", "retrieve"]:
            return LivroListRetrieveSerializer
        return LivroSerializer

Nesse caso, o serializador LivroListRetrieveSerializer é utilizado para a listagem e recuperação de um único livro, enquanto o LivroSerializer é utilizado para as demais operações, ou seja, criação e alteração.

Criação de um serializador para a listagem de livros

Podemos criar um serializador para a listagem de livros, que mostre apenas o id, o título e o preço. Isso pode ser útil, pois traz menos informações, o que pode tornar a listagem mais rápida.

from core.serializers import (
    LivroListSerializer,
    LivroRetrieveSerializer,
    LivroSerializer,
)
...
class LivroListSerializer(ModelSerializer):
    class Meta:
        model = Livro
        fields = ("id", "titulo", "preco")

class LivroRetrieveSerializer(ModelSerializer):
    class Meta:
        model = Livro
        fields = "__all__"
        depth = 1
    def get_serializer_class(self):
        if self.action == "list":
            return LivroListSerializer
        elif self.action == "retrieve":
            return LivroRetrieveSerializer
        return LivroSerializer

Observe que o serializador LivroListSerializer é utilizado apenas na listagem, enquanto o LivroRetrieveSerializer é utilizado na recuperação de um único livro e o LivroSerializer é utilizado nas demais operações.

from .livro import LivroListSerializer, LivroRetrieveSerializer, LivroSerializer

12. Upload e associação de imagens

Vamos instalar uma aplicação para gerenciar o upload de imagens e sua associação ao nosso modelo. Com isso poderemos associar imagens aos livros, ao perfil do usuário, etc.

Essa aplicação não será instalada através do comando pdm add <pacote>, pois é uma aplicação que não está disponível no PyPI. Ela será instalada manualmente, baixando e descompactando um arquivo compactado.

Baixando o pacote

Baixe e descompacte o arquivo com a app pronta para ser utilizada.

wget https://github.com/marrcandre/django-drf-tutorial/raw/main/apps/uploader.zip -O uploader.zip && unzip uploader.zip && rm -v uploader.zip
Invoke-WebRequest -Uri https://github.com/marrcandre/django-drf-tutorial/raw/main/apps/uploader.zip -OutFile uploader.zip
Expand-Archive -Path uploader.zip -DestinationPath .
Remove-Item -Force uploader.zip

O projeto ficará com uma estrutura parecida com essa:

App Uploader

Instalando as dependências

rm -rf __pypackages__ pdm.lock
pdm lock
pdm install
pdm add Pillow
pdm add "python-magic; sys_platform=='linux'"
pdm add "python-magic-bin; sys_platform=='win32, darwin'"

O pacote python-magic é utilizado para identificar o tipo de arquivo, enquanto o Pillow é utilizado para manipulação de imagens.

O pacote python-magic-bin é utilizado no Windows e MacOS, enquanto o python-magic é utilizado no Linux.

Registro da app

INSTALLED_APPS = [
    ...
    "uploader", # nova linha
    "core",
    ...
]

IMPORTANTE: Não esqueça da vírgula no final da linha.

Configuração no settings.py

# App Uploader settings
MEDIA_ENDPOINT = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
FILE_UPLOAD_PERMISSIONS = 0o640

Configuração no urls.py

from django.conf import settings
from django.conf.urls.static import static
...
from uploader.router import router as uploader_router
...
urlpatterns = [
    ...
    path("api/media/", include(uploader_router.urls)),  # nova linha
    ...
]
...
urlpatterns += static(settings.MEDIA_ENDPOINT, document_root=settings.MEDIA_ROOT)
...
post_migrate = "python manage.py graph_models --disable-sort-fields -g -o core.png core uploader"

Migração do banco de dados

pdm run migrate

Uso em modelos

Agora que a aplicação uploader está configurada, vamos utilizá-la para associar imagens aos livros.

...
from uploader.models import Image


class Livro(models.Model):
...
    capa = models.ForeignKey(
        Image,
        related_name="+",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        default=None,
    )

O campo capa é uma chave estrangeira para a tabela uploader_image.

O atributo related_name="+" indica que não será criado um atributo inverso na tabela uploader_image.

O atributo on_delete=models.SET_NULL indica que, ao apagar a imagem, o campo capa será setado como NULL.

pdm run migrate

O modelo Livro ficará assim:

Projeto com a model Livro com capa

Observe que o campo capa_id foi criado na tabela core_livro, fazendo referência à tabela uploader_image.

Uso no serializer

...
from rest_framework.serializers import ModelSerializer, SlugRelatedField

from uploader.models import Image
from uploader.serializers import ImageSerializer
...
class LivroSerializer(ModelSerializer):
    capa_attachment_key = SlugRelatedField(
        source="capa",
        queryset=Image.objects.all(),
        slug_field="attachment_key",
        required=False,
        write_only=True,
    )
    capa = ImageSerializer(
        required=False,
        read_only=True
    )

...
class LivroListRetrieveSerializer(ModelSerializer):
...
    capa = ImageSerializer(required=False)

Alteramos dois serializadores: um para a gravação e outro para a recuperação de um único livro.

O campo capa_attachment_key é utilizado para a gravação da imagem, enquanto o campo capa é utilizado para a recuperação da imagem.

Teste de upload e associação com o livro

13. Dump e Load de dados

O dump dos dados permite que você salve os dados do banco de dados em um arquivo. O load dos dados permite que você carregue os dados de um arquivo para o banco de dados. Isso é útil para fazer cópias de segurança, para transferir dados entre bancos de dados, para carregar dados iniciais, etc.

Carga inicial de dados

Cópia de segurança dos dados

pdm run dumpdata > core.json
code core.json

IMPORTANTE: Se o arquivo tiver algumas linhas semelhantes a essas no seu início, apague-as:

MODE = 'DEVELOPMENT'
MEDIA_URL = 'http://191.52.55.44:19003/media/'
DATABASES = {'default': {'NAME': 'db.sqlite3', 'USER': '', 'PASSWORD': '', 'HOST': '', 'PORT': '', 'CONN_MAX_AGE': 600, 'CONN_HEALTH_CHECKS': True, 'DISABLE_SERVER_SIDE_CURSORS': False, 'ENGINE': 'django.db.backends.sqlite3'}}

Arquivo exemplo:

No Linux:

wget https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/core.json

No Windows:

Invoke-WebRequest -Uri "https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/core.json" -OutFile core.json

Carga dos dados

pdm run loaddata

O comando espera um arquivo core.json na pasta raiz do projeto.

Verificando se a carga dos dados funcionou

pdm run shellp

E dentro dele, execute:

>>> Livro.objects.all()

Você também pode acessar o Django Admin ou o Swagger e verificar que os dados foram carregados.

14. Customização do Admin

O Admin é uma ferramenta para gerenciar os dados do banco de dados. Ele pode ser customizado para melhorar a experiência do usuário.

Importação das models

Vamos importar as models de forma explícita:

from core.models import Autor, Categoria, Editora, Livro, User

Registro das models através do decorator @admin.register

Vamos registrar as models através do decorator @admin.register, ao invés de utilizar a função admin.site.register(). Por exemplo, para a model User:

@admin.register(User)
class UserAdmin(BaseUserAdmin):
...

Customização do Admin

Vamos customizar o Admin para as models Autor, Categoria, Editora e Livro:

...
@admin.register(Autor)
class AutorAdmin(admin.ModelAdmin):
    list_display = ('nome', 'email')
    search_fields = ('nome', 'email')
    list_filter = ('nome',)
    ordering = ('nome', 'email')
    list_per_page = 10

@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
    list_display = ('descricao',)
    search_fields = ('descricao',)
    list_filter = ('descricao',)
    ordering = ('descricao',)
    list_per_page = 10

@admin.register(Editora)
class EditoraAdmin(admin.ModelAdmin):
    list_display = ('nome', 'email', 'cidade')
    search_fields = ('nome', 'email', 'cidade')
    list_filter = ('nome', 'email', 'cidade')
    ordering = ('nome', 'email', 'cidade')
    list_per_page = 10

@admin.register(Livro)
class LivroAdmin(admin.ModelAdmin):
    list_display = ('titulo', 'editora', 'categoria')
    search_fields = ('titulo', 'editora__nome', 'categoria__descricao')
    list_filter = ('editora', 'categoria')
    ordering = ('titulo', 'editora', 'categoria')
    list_per_page = 25

O atributo list_display é uma tupla que define os campos que serão exibidos na listagem.

O atributo search_fields é uma tupla que define os campos que serão utilizados na busca.

O atributo list_filter é uma tupla que define os campos que serão utilizados para filtrar os registros.

O atributo ordering é uma tupla que define a ordem de exibição default dos registros.

15. Uso do Django Shell e do Django Shell Plus

O Django Shell é uma ferramenta para interagir com o banco de dados. O Django Shell Plus é uma extensão do Django Shell que inclui alguns recursos adicionais, como a inclusão automática dos modelos.

pdm run shellp
>>> categoria = Categoria.objects.create(descricao="Desenvolvimento Web")
>>> categoria
<Categoria: Desenvolvimento Web>
>>> Categoria.objects.all()
<QuerySet [<Categoria: Desenvolvimento Web>]>
>>> categoria = Categoria.objects.get(descricao="Desenvolvimento Web")
>>> categoria
<Categoria: Desenvolvimento Web>
>>> categoria.descricao = "Desenvolvimento Web com Django"
>>> categoria.save()
>>> categoria
<Categoria: Desenvolvimento Web com Django>
>>> categoria.delete()
(1, {'core.Categoria': 1})
>>> Categoria.objects.all()
<QuerySet []>

Usando o atributo related_name

Autor.objects.get(id=1).livros.all()
Categoria.objects.get(id=1).livros.all()
Editora.objects.get(id=1).livros.all()
>>> exit()

16. Autenticação e autorização

Introdução

Vamos trabalhar agora os conceitos de segurança relacionados a autenticação (login) e autorização (permissão). Utilizaremos aquilo que o Django já oferece, em termos de usuários e grupos.

Uma estratégia muito utilizada para a definição de permissões de acesso é:

Resumindo: toda a estratégia de permissões parte da criação de grupos e inclusão ou remoção de usuários desses grupos.

Observe no Admin, para cada usuário em Usuários (Users), as opções de Permissões do usuário.

Relação entre nomes das ações

Podemos perceber uma relação as ações que compõem o CRUD, os termos utilizados no Admin e os verbos HTTP e as actions dos serializadores do Django REST Framework.:

Ação CRUD Admin HTTP FDRF Actions
Criar Create add POST create
Ler Read view GET retrieve, list
Atualizar Update change PUT (PATCH) update, partial_update
Deletar Delete delete DELETE destroy

Exercício:

No Admin, crie os seguintes usuários e grupos e dê as permissões necessárias:

a. Criando grupos e dando permissões

Vamos começar criando 2 grupos e dando a eles permissões distintas:

b. Criando usuários e adicionando aos grupos

17. Utilização das permissões do DRF

Autenticação e permissão

A autenticação ou identificação por si só geralmente não é suficiente para obter acesso à informação ou código. Para isso, a entidade que solicita o acesso deve ter autorização. (Permissões no DRF)

Autenticação significa que um usuário foi identificado em um sistema, portanto ele é conhecido. Isso se dá, normalmente por um sistema de login.

Permissão (autorização) se dá por um esquema de conceder privilégios, seja a usuários ou grupos.

Por padrão, qualquer usuário, mesmo sem autenticação, tem acesso irrestrito e permissão de fazer qualquer coisa em uma aplicação.

As permissões podem ser definidas:

  1. a nível de objeto (nas views ou viewsets, por exemplo);
  2. de forma global, no arquivo settings.py;
  3. com o uso de classes de permissão do Django REST Framework.

Vamos analisar cada uma dessas formas.

a. Exemplo de uso de permissão na viewset

Vamos ver um exemplo de uso de permissão em uma viewset. No exemplo, vamos permitir acesso apenas a usuários autenticados na model Categoria.

Como ilustração, modifique o arquivo views/categoria.py, da seguinte forma.

from rest_framework.permissions import IsAuthenticated
permission_classes = [IsAuthenticated]

Para testar:

Resumindo, utilizamos a classe IsAuthenticated para permitir acesso apenas a usuários autenticados.

b. Exemplo de uso de permissão no settings.py

Outra forma de gerenciamento de permissões é feita no arquivo settings.py.

IMPORTANTE: Para utilizá-la, comente as últimas alterações feitas no arquivo views/categoria.py.

Uma forma de conseguir o mesmo resultado de forma padrão para todo o projeto, isto é, permitir acesso aos endpoints apenas para usuários autenticados, é configurar desse modo o arquivo settings.py:

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ]
}

Para testar:

Resumindo, utilizamos a classe IsAuthenticated no settings.py para permitir acesso apenas a usuários autenticados.

c. Permissões com o DjangoModelPermissionsOrAnonReadOnly

Apesar de ser possível definir a autorização das formas que vimos anteriormente, adotaremos uma outra forma. Essa forma que iremos adotar para o gerenciamento de permissões será com o uso do DjangoModelPermissions.

Esta classe de permissão está ligada às permissões do modelo django.contrib.auth padrão do Django. Essa permissão deve ser aplicada apenas a visualizações que tenham uma propriedade .queryset ou método get_queryset() (exatamente o que temos).

A autorização só será concedida se o usuário estiver autenticado e tiver as permissões de modelo relevantes atribuídas, da seguinte forma:

Para isso, teremos que alterar a classe de autenticação, substituindo o que colocamos anteriormente:

REST_FRAMEWORK = {
    ...
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly", ),  # autorização de acesso
    ...
}

Resumindo, utilizaremos a estrutura de usuários, grupos e permissões que o próprio Django já nos fornece. Para isso, utilizaremos o DjangoModelPermissionsOrAnonReadOnly para gerenciar as permissões.

Para utilizar essa estrutura de permissões corretamente, precisaremos de um sistema de autenticação (login) no nosso projeto, de forma a enviar essas informações via a URL. Para isso, utilizaremos o Passage.

18. Autenticação com Passage

Criação da conta no Passage

Se você ainda não tem uma conta no Passage:

Criação de um aplicativo no Passage

Após criar a conta, você deve criar um aplicativo:

Importante: o domínio e a porta devem ser os mesmos que você está utilizando para desenvolver o seu PWA. No nosso caso, estamos utilizando o domínio http://localhost:5173. Quando você for colocar o seu PWA em produção, você deve alterar o domínio para o domínio do seu site.

Configuração do Passage no backend Django

REST_FRAMEWORK = {
    ...
    "DEFAULT_AUTHENTICATION_CLASSES": ("core.authentication.TokenAuthentication",), # Autenticação no passage.id
    "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly", ),  # autorização de acesso
    ...
}

No arquivo .env, preencha as seguintes variáveis com os valores da sua aplicação:

PASSAGE_APP_ID=sua_app_id
PASSAGE_APP_KEY=sua_app_key

Configuração do Passage no frontend Vue

    <passage-auth app-id="seu_app_id"></passage-auth>

Substitua o valor de app-id pelo valor do seu app_id, no Passage.

19. Inclusão da foto de perfil no usuário

Vamos aproveitar a aplicação uploader para incluir a foto de perfil no usuário.

Criação do campo de foto de perfil

...
from uploader.models import Image
...
class User(AbstractUser):
    foto = models.ForeignKey(
        Image,
        related_name="+",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        default=None,
    )

O campo foto é uma chave estrangeira para a tabela uploader_image.

A foto será opcional, por isso utilizamos null=True e blank=True.

O campo foto será null por padrão, por isso utilizamos default=None.

Se a foto for deletada, o campo foto será null, por isso utilizamos on_delete=models.SET_NULL.

Seu projeto deve ficar assim:

Projeto com a model Livro

Observe a ligação entre a model User e a model Image, através da chave estrangeira foto.

Inclusão da foto no Admin

...
class UserAdmin(UserAdmin):
    ...
        (_("Personal Info"), {"fields": ("name","foto")}), # inclua a foto aqui

    ...

Inclusão da foto no Serializer

from rest_framework.serializers import ModelSerializer, SlugRelatedField

from core.models import User
from uploader.models import Image
from uploader.serializers import ImageSerializer


class UserSerializer(ModelSerializer):
    foto_attachment_key = SlugRelatedField(
        source="foto",
        queryset=Image.objects.all(),
        slug_field="attachment_key",
        required=False,
        write_only=True,
    )
    foto = ImageSerializer(
        required=False,
        read_only=True
    )

    class Meta:
        model = User
        fields = "__all__"

O atributo write_only=True indica que o campo foto_attachment_key é apenas para escrita. Isso significa que ele não será exibido na resposta da API.

O atributo read_only=True indica que o campo foto é apenas para leitura. Isso significa que ele não será aceito na requisição da API.

Testando

Finalizando

20. Criação da entidade Compra integrada ao usuário do projeto

A partir dessa aula, vamos implementar o processo de compra de livros, na nossa aplicação. Nessa aula, vamos criar um entidade de compras integrada à entidade do usuário do projeto.

Criando o model de compras

touch core/models/compra.py
from django.db import models

from .user import User

class Compra(models.Model):
    class StatusCompra(models.IntegerChoices):
        CARRINHO = 1, "Carrinho"
        FINALIZADO = 2, "Realizado"
        PAGO = 3, "Pago"
        ENTREGUE = 4, "Entregue"

    usuario = models.ForeignKey(User, on_delete=models.PROTECT, related_name="compras")
    status = models.IntegerField(choices=StatusCompra.choices,  default=StatusCompra.CARRINHO)

Note que estamos utilizando a model User como ForeignKey para a model Compra.

StatusCompra é do tipo IntegerChoices, que é uma forma de criar um campo choices com valores inteiros.

status é um campo IntegerField que utiliza o choices StatusCompra.choices e tem o valor padrão StatusCompra.CARRINHO, que no caso é 1.

Opcionalmente, poderíamos ter criado uma entidade StatusCompra e utilizado um campo ForeignKey para ela. No entanto, como temos um número pequeno de status, optamos por utilizar o IntegerField com choices.

from .compra import Compra

Adicionando a model Compra ao Admin

...
from core.models import Compra

admin.site.register(Compra)

Executando as migrações

O seu projeto deve ficar assim:

Projeto com a model Compra

Testando a model Compra

Finalizando

21. Criação dos itens da compra

No caso dos itens da compra, não vamos utilizar um campo livro do tipo ManyToManyField no model Compra, pois queremos ter a possibilidade de adicionar mais informações ao item da compra, como a quantidade, por exemplo. Desta forma, vamos criar “manualmente” a entidade associativa, que será chamada de ItensCompra.

...
from .livro import Livro
...
class ItensCompra(models.Model):
    compra = models.ForeignKey(Compra, on_delete=models.CASCADE, related_name="itens")
    livro = models.ForeignKey(Livro, on_delete=models.PROTECT, related_name="+")
    quantidade = models.IntegerField(default=1)

No atributo compra, utilizamos models.CASCADE, pois queremos que, ao deletar uma compra, todos os itens da compra sejam deletados também.

No atributo livro, utilizamos models.PROTECT, pois queremos impedir que um livro seja deletado se ele estiver associado a um item de compra.

Ainda no livro, utilizamos related_name="+", pois não queremos que o ItensCompra tenha um atributo livro.

from .compra import Compra, ItensCompra

O seu projeto deve ficar assim:

Projeto com a model Compra

22. Uso de TabularInline no Admin para Itens da Compra

Da forma que configuramos o Admin para a model ItensCompra, não é possível adicionar itens da compra diretamente na tela de edição da compra. Isso é pouco natural, pois há uma relação direta entre a compra e seus itens.

Sendo assim, vamos mostrar os itens da compra no Admin do Django, utilizando o TabularInline. Desta forma, podemos adicionar os itens da compra diretamente na tela de edição da compra.

class ItensCompraInline(admin.TabularInline):
    model = ItensCompra
    extra = 1 # Quantidade de itens adicionais


@admin.register(Compra)
class CompraAdmin(admin.ModelAdmin):
    list_display = ("usuario", "status")
    search_fields = ("usuario", "status")
    list_filter = ("usuario", "status")
    ordering = ("usuario", "status")
    list_per_page = 25
    inlines = [ItensCompraInline]

Desta forma, quando você editar uma compra no Admin do Django, você verá os itens da compra logo abaixo do formulário de edição da compra.

Opcionalmente, você pode utilizar o StackedInline ao invés do TabularInline. Experimente e veja a diferença.

23. Endpoint para a listagem básica de compras

Vamos começar a criar os endpoints para a entidade Compra, começando pela listagem básica de compras. Posteriormente, vamos incluir os itens da compra e criar os endpoints para adicionar, editar e excluir compras.

Criação do serializer de Compra

touch core/serializers/compra.py
from rest_framework.serializers import ModelSerializer

from core.models import Compra

class CompraSerializer(ModelSerializer):
    class Meta:
        model = Compra
        fields = "__all__"
from .compra import CompraSerializer

Criação da Viewset de Compra

touch core/views/compra.py
from rest_framework.viewsets import ModelViewSet

from core.models import Compra
from core.serializers import CompraSerializer


class CompraViewSet(ModelViewSet):
    queryset = Compra.objects.all()
    serializer_class = CompraSerializer
from .compra import CompraViewSet

URL para listagem de compras

...
from core.views import (
    AutorViewSet,
    CategoriaViewSet,
    CompraViewSet, # inclua essa linha
    EditoraViewSet,
    LivroViewSet,
    UserViewSet,
)
...
router.register(r"compras", CompraViewSet)
...

Inclusão do email do usuário na listagem da compra

Nesse momento, a listagem de compras mostra apenas o id do usuário. Vamos substituir o id pelo email do usuário.

...
from rest_framework.serializers import CharField, ModelSerializer
...
class CompraSerializer(ModelSerializer):
    usuario = CharField(source="usuario.email", read_only=True) # inclua essa linha
...

O parâmetro source indica qual campo do model Compra será utilizado para preencher o campo usuario do serializer.

O parâmetro read_only indica que o campo usuario não será utilizado para atualizar o model Compra.

Inclusão do status da compra na listagem da compra

De forma semelhante ao email do usuário, vamos incluir o status da compra na listagem da compra.

...
class CompraSerializer(ModelSerializer):
    status = CharField(source="get_status_display", read_only=True) # inclua essa linha
...

O parâmetro source indica qual método do model Compra será utilizado para preencher o campo status do serializer. Sempre que utilizamos um campo do tipo IntegerChoices, podemos utilizar o método get_<nome_do_campo>_display para obter a descrição do campo.

O parâmetro read_only indica que o campo status não será utilizado para atualizar o model Compra.

Estes são apenas dois exemplos de como podemos modificar a listagem de compras. Você pode incluir outros campos, como o total da compra, por exemplo.

24. Visualização dos itens da compra no endpoint da listagem de compras

De forma semelhante ao que fizemos no Admin, vamos incluir os itens da compra na listagem de compras.

...
from core.models import Compra, ItensCompra
...

class ItensCompraSerializer(ModelSerializer):
    class Meta:
        model = ItensCompra
        fields = "__all__"

No CompraSerializer, inclua o seguinte código:

...
itens = ItensCompraSerializer(many=True, read_only=True)
...

O parâmetro many=True indica que o campo itens é uma lista de itens.

O parâmetro read_only=True indica que o campo itens não será utilizado para atualizar o model Compra.

Mostrando os detalhes dos itens da compra na listagem de compras

class ItensCompraSerializer(ModelSerializer):
    class Meta:
        model = ItensCompra
        fields = "__all__"
        depth = 1

O parâmetro depth=1 indica que o serializer deve mostrar os detalhes do model ItensCompra. O valor 1 indica que o serializer deve mostrar os detalhes do model ItensCompra e dos models relacionados a ele (nesse caso, o livro). Se o valor fosse 2, o serializer mostraria os detalhes do model ItensCompra, dos models relacionados a ele e dos models relacionados aos models relacionados a ele (nesse caso, a categoria, a editora e o autor).

Mostrando apenas os campos necessários dos itens da compra na listagem de compras

Você deve ter percebido que o serializer de ItensCompra está mostrando todos os seus campos, incluindo o campo compra. Vamos modificar o serializer para mostrar apenas os campos necessários. Nesse exemplo, vamos mostrar apenas os camposlivro e quantidade.

fields = ("livro", "quantidade")

O parâmetro fields indica quais campos do model ItensCompra serão mostrados no serializer. Se o valor for __all__, todos os campos serão mostrados. Se o valor for uma sequência de campos, apenas esses campos serão mostrados.

25. Exibição do total do item na listagem de compras

O total do item é calculado pelo preço do livro multiplicado pela quantidade. Esse é um campo calculado, que não existe no model ItensCompra. Vamos incluir esse campo na listagem de compras.

from rest_framework.serializers import CharField, ModelSerializer, SerializerMethodField
class ItensCompraSerializer(ModelSerializer):
    total = SerializerMethodField()

    def get_total(self, instance):
        return instance.livro.preco * instance.quantidade

    class Meta:
        model = ItensCompra
        fields = ("livro", "quantidade", "total")
        depth = 1

O parâmetro SerializerMethodField indica que o campo total não existe no model ItensCompra. Ele será calculado pelo método get_total.

O método get_total recebe como parâmetro o objeto instance, que representa o item da compra. A partir dele, podemos acessar os campos do item da compra, como quantidade e livro.preco.

O método get_total retorna o valor do campo total, que é calculado pelo preço do livro multiplicado pela quantidade.

O método get_<nome_do_campo> é um método especial do serializer que é chamado para calcular o valor do campo <nome_do_campo>.

Incluimos o campo total no atributo fields do serializer.

26. Inclusão do total da compra na listagem de compras

Vamos incluir o total da compra na listagem de compras. O total da compra é calculado pela soma dos totais dos itens da compra. Esse é um campo calculado, que não existe no model Compra. Vamos incluir esse campo na listagem de compras.

...
    @property
    def total(self):
        # total = 0
        # for item in self.itens.all():
        #     total += item.livro.preco * item.quantidade
        # return total
        return sum(item.livro.preco * item.quantidade for item in self.itens.all())

No código acima, temos duas formas de calcular o total da compra. A primeira forma está comentada. A segunda forma está descomentada. A segunda forma é mais simples e mais eficiente, e utiliza uma compreensão de lista (list comprehension).

O método property indica que o campo total não existe no model Compra. Ele será calculado pelo método total.

O método total retorna o valor do campo total, que é calculado pela soma dos totais dos itens da compra, que é calculado pelo preço do livro multiplicado pela quantidade do item da compra.

...
        fields = ("id", "usuario", "status", "total", "itens")
...

O parâmetro fields indica quais campos do model Compra serão mostrados no serializer. Se o valor for __all__, todos os campos serão mostrados. Se o valor for uma lista de campos, apenas os campos da lista serão mostrados, na ordem da lista.

27. Criação de um endpoint para criar novas compras

Vamos primeiro definir o que é necessário para criar uma nova compra. Para criar uma nova compra, precisamos informar o usuário e os itens da compra. Os itens da compra são compostos pelo livro e pela quantidade. Essas são as informações necessárias para criar uma nova compra.

O formato dos dados para criar uma nova compra é o seguinte:

{
    "usuario": 1,
    "itens": [
        {
            "livro": 1,
            "quantidade": 1
        },
        {
            "livro": 2,
            "quantidade": 2
        }
    ]
}

Criando um serializer para os itens da compra

Tendo definido o formato dos dados, vamos criar um novo serializer, que será usado para criar uma nova compra ou editar uma compra já existente.

...
class CompraCreateUpdateSerializer(ModelSerializer):
    itens = ItensCompraSerializer(many=True)

    class Meta:
        model = Compra
        fields = ("usuario", "itens")
...

O parâmetro many=True indica que o campo itens é uma lista de itens de compra.

from .compra import CompraSerializer, CompraCreateUpdateSerializer, ItensCompraSerializer

Alterando o viewset de Compra para usar o novo serializer

Vamos alterar o viewset de Compra para usar o novo serializer, nas operações de criação e edição.

...
from core.serializers import CompraSerializer, CompraCreateUpdateSerializer
...
class CompraViewSet(ModelViewSet):
    queryset = Compra.objects.all()
    serializer_class = CompraSerializer

    def get_serializer_class(self):
        if self.action in ("create", "update"):
            return CompraCreateUpdateSerializer
        return CompraSerializer
...
{
    "usuario": 1,
    "itens": [
        {
            "livro": 1,
            "quantidade": 1
        }
    ]
}

Você receberá o seguinte erro:

AssertionError at /api/compras/ The .create() method does not support writable nested fields by default. Write an explicit .create() method for serializer core.serializers.compra.CompraCreateUpdateSerializer, or set read_only=True on nested serializer fields.

Traduzindo, chegamos no seguinte:

Erro de afirmação em /api/compras/ O método .create() não suporta campos aninhados graváveis por padrão. Escreva um método .create() explícito para o serializer core.serializers.compra.CompraCreateUpdateSerializer, ou defina read_only=True nos campos do serializer aninhado.

O erro ocorre por que os itens da compra vêm de outra tabela, a tabela ItemCompra, através de uma chave estrangeira. O serializer de Compra não sabe como criar os itens da compra. Precisamos alterar o método create do serializer de Compra para criar os itens da compra.

Alterando o método create do serializer de Compra

...

class CompraCreateUpdateSerializer(ModelSerializer):
    itens = ItensCompraCreateUpdateSerializer(many=True) # Aqui mudou

    class Meta:
        model = Compra
        fields = ("usuario", "itens")

    def create(self, validated_data):
        itens_data = validated_data.pop("itens")
        compra = Compra.objects.create(**validated_data)
        for item_data in itens_data:
            ItensCompra.objects.create(compra=compra, **item_data)
        compra.save()
        return compra

O método create é chamado quando uma nova compra é criada. Ele recebe os dados validados e cria a compra e os itens da compra.

O método create recebe um parâmetro validated_data, que são os dados validados que estão sendo criados.

validade_data.pop("itens") remove os itens da compra dos dados validados.

...
class ItensCompraCreateUpdateSerializer(ModelSerializer):
    class Meta:
        model = ItensCompra
        fields = ("livro", "quantidade")
...

O serializer de ItensCompra é bem simples, pois ele recebe apenas o livro e a quantidade.

28. Criação de um endpoint para atualizar compras

{
    "usuario": 2,
    "itens": [
        {
            "livro": 2,
            "quantidade": 2
        }
    ]
}

Você receberá o seguinte erro:

AssertionError at /api/compras/1/ The .update() method does not support writable nested fields by default. Write an explicit .update() method for serializer core.serializers.compra.CompraCreateUpdateSerializer, or set read_only=True on nested serializer fields.

Traduzindo:

Erro de afirmação em /api/compras/1/ O método .update() não suporta campos aninhados graváveis por padrão. Escreva um método .update() explícito para o serializer core.serializers.compra.CompraCreateUpdateSerializer, ou defina read_only=True nos campos do serializer aninhado.

O erro ocorre por que os itens da compra vêm de outra tabela, a tabela ItensCompra, através de uma chave estangeira. O serializer de Compra não sabe como atualizar os itens da compra. Precisamos alterar o método update do serializer de Compra para atualizar os itens da compra.

...
    def update(self, compra, validated_data):
        itens_data = validated_data.pop("itens")
        if itens_data:
            compra.itens.all().delete()
            for item_data in itens_data:
                ItensCompra.objects.create(compra=compra, **item_data)
        return super().update(compra, validated_data)
...

O método update é chamado quando uma compra é atualizada. Ele recebe os dados validados e atualiza a compra e os itens da compra.

O método update recebe dois parâmetros: compra e validated_data. O parâmetro compra é a compra que está sendo atualizada. O parâmetro validated_data são os dados validados que estão sendo atualizados.

O comando compra.itens.all().delete() remove todos os itens da compra (se houverem)

O comando ItensCompra.objects.create(compra=compra, **item_data) cria novos itens com os dados validados.

O comando super().update(compra, validated_data) chama o método update da classe pai, que é o método padrão de atualização.

28b. Criação de um serializador específico para a listagem de compras

Como fizemos com o Livro, vamos criar um serializador específico para a listagem de compras, que vai mostrar apenas os campos necessários. Com isso, a listagem de compras ficará mais enxuta.

...
class CompraListSerializer(ModelSerializer):
    usuario = CharField(source="usuario.email", read_only=True)
    itens = ItensCompraListSerializer(many=True, read_only=True)

    class Meta:
        model = Compra
        fields = ("id", "usuario", "itens")
...

O serializer CompraListSerializer é um serializer específico para a listagem de compras. Ele mostra apenas os campos necessários.

Vamos criar também um serializador específico para os itens da compra:

...
class ItensCompraListSerializer(ModelSerializer):
    livro = CharField(source="livro.titulo", read_only=True)

    class Meta:
        model = ItensCompra
        fields = ("quantidade", "livro")
        depth = 1
...

Temos que incluir o novo serializer no arquivo __init__.py dos serializers:

...
from .compra import (
    CompraListSerializer, # novo
    CompraCreateUpdateSerializer,
    CompraSerializer,
    ItensCompraCreateUpdateSerializer,
    ItensCompraListSerializer, # novo
    ItensCompraSerializer,
)
...
...
from .serializers import CompraCreateUpdateSerializer, CompraListSerializer, CompraSerializer
...
class CompraViewSet(ModelViewSet):
...
    def get_serializer_class(self):
        if self.action == "list":
            return CompraListSerializer
        if self.action in ("create", "update"):
            return CompraCreateUpdateSerializer
        return CompraSerializer
...

29. Criação de uma compra a partir do usuário autenticado

Ao invés de passar o usuário no corpo da requisição, podemos pegar o usuário autenticado e criar a compra a partir dele. O Django Rest Framework nos dá uma forma de fazer isso.

from rest_framework.serializers import (
    CharField,
    CurrentUserDefault, # novo
    HiddenField, # novo
    ModelSerializer,
    SerializerMethodField,
)
class CompraCreateUpdateSerializer(ModelSerializer):
    usuario = HiddenField(default=CurrentUserDefault())
...

O campo usuario é um campo oculto, pois foi definido como HiddenField. Ele não é exibido no serializer.

O valor padrão do campo é o usuário autenticado.

O CurrentUserDefault é um campo padrão que retorna o usuário autenticado.

Para testar, vamos criar uma nova compra no endpoint compras/ no ThunderClient, utilizando o método POST:

{
    "itens": [
        {
            "livro": 2,
            "quantidade": 2
        }
    ]
}

Observe que não precisamos mais passar o usuário no corpo da requisição, pois ele pega o usuário autenticado.

30. Filtragem de apenas as compras do usuário autenticado

Nesse momento, qualquer usuário pode ver todas as compras. Vamos filtrar da seguinte forma: se o usuário for um usuário normal, ele só pode ver as suas compras. Se o usuário for um administrador, ele pode ver todas as compras.

...
class CompraViewSet(ModelViewSet):
    queryset = Compra.objects.all()

    def get_queryset(self):
        usuario = self.request.user
        if usuario.is_superuser:
            return Compra.objects.all()
        if usuario.groups.filter(name="Administradores"):
            return Compra.objects.all()
        return Compra.objects.filter(usuario=usuario)
...

O método get_queryset é chamado quando uma compra é listada.

O request é o objeto que representa a requisição. O request.user é o usuário autenticado.

Se o usuário for um superusuário ou for membro do grupo “Administradores”, retorna todas as compras. Caso contrário, retorna apenas as compras do usuário autenticado.

31. Validação dos campos no serializer

Não permitindo itens com quantidade zero

Nesse momento, é possível criar uma compra com um item com quantidade zero. Vamos validar isso.

...
from rest_framework.serializers import (
    CharField,
    CurrentUserDefault,
    HiddenField,
    ModelSerializer,
    SerializerMethodField,
    ValidationError, # novo
)

class ItensCompraCreateUpdateSerializer(ModelSerializer):
    class Meta:
        model = ItensCompra
        fields = ("livro", "quantidade")

    def validate_quantidade(self, quantidade):
        if quantidade <= 0:
            raise ValidationError("A quantidade deve ser maior do que zero.")
        return quantidade
...

A função validate_<nome_do_campo> é chamada quando um campo é validado. Nesse caso, ela está verificando se a quantidade do item da compra (quantidade) é maior do que zero.

Se a quantidade for menor ou igual a zero, é lançada uma exceção ValidationError.

Não permitindo quantidade de itens maior do que a quantidade em estoque

Nesse momento, é possível criar uma compra com uma quantidade de itens maior do que a quantidade em estoque. Vamos validar isso.

...
from rest_framework.serializers import (
    CharField,
    CurrentUserDefault,
    HiddenField,
    ModelSerializer,
    SerializerMethodField,
    ValidationError, # novo
)
...
    def validate(self, item):
        if item["quantidade"] > item["livro"].quantidade:
            raise ValidationError("Quantidade de itens maior do que a quantidade em estoque.")
        return item
...

A função validate permite adicionar validações de campo que dependem de múltiplos valores ao mesmo tempo. Nesse caso, ela está verificando se a quantidade solicitada do item (item["quantidade"]) não excede a quantidade disponível em estoque (item["livro"].quantidade).

Formatando dados antes de salvar

Podemos usar as funções de validação para formatar os dados antes de salvar. Por exemplo, podemos gravar o email da Editora em minúsculas.

...
    def validate_email(self, email):
        return email.lower()
...

A função validate_<nome_do_campo> é chamada quando um campo é validado. Nesse caso, ela está formatando o email da Editora em minúsculas.

32. Gravação do preço do livro no item da compra

Nesse momento, o preço do livro não é gravado no item da compra. Vamos gravar o preço do livro no item da compra, uma vez que o preço do livro pode mudar e queremos manter o registro do preço do livro no momento da compra.

Inclui o campo preco na entidade ItensCompra

...
class ItensCompra(models.Model):
...
    preco = models.DecimalField(max_digits=10, decimal_places=2, default=0)
...

Gravando o preço do livro na criação do item da compra

...
    def create(self, validated_data):
        itens = validated_data.pop("itens")
        compra = Compra.objects.create(**validated_data)
        for item in itens:
            item["preco"] = item["livro"].preco # nova linha
            ItensCompra.objects.create(compra=compra, **item)
        compra.save()
        return compra
...

O método create é chamado quando uma nova compra é criada. Ele recebe os dados validados e cria a compra e os itens da compra.

Alterando o campo total da compra para considerar o preço do item da compra

...
    @property
    def total(self):
        return sum(item.preco * item.quantidade for item in self.itens.all())
...

Estamos utilizando o campo preco para calcular o total da compra, ao invés do campo livro.preco.

Gravando o preço do livro na atualização do item da compra

Da mesma forma, precisamos alterar o método update do serializer CompraCreateUpdateSerializer para gravar o preço do livro no item da compra:

...
    def update(self, compra, validated_data):
        itens = validated_data.pop("itens")
        if itens:
            compra.itens.all().delete()
            for item in itens:
                item["preco"] = item["livro"].preco  # nova linha
                ItensCompra.objects.create(compra=compra, **item)
        compra.save()
        return super().update(compra, validated_data)
...

33. Inclusão da data da compra

No momento, não existe nenhum registro da data da compra. Vamos incluir a data da compra, que será definida automaticamente no momento da criação da compra.

...
class Compra(models.Model):
...
    usuario = models.ForeignKey(User, on_delete=models.PROTECT, related_name="compras")
    status = models.IntegerField(choices=StatusCompra.choices, default=StatusCompra.CARRINHO)
    data = models.DateTimeField(auto_now_add=True) # campo novo
...

O campo data é um campo do tipo DateTimeField, que armazena a data e a hora da compra.

O parâmetro auto_now_add=True indica que o campo será preenchido automaticamente com a data e hora atual, quando a compra for criada.

Você receberá um erro na migration, pois o campo data não pode ser nulo.

Modificando o serializer de compra para mostrar a data da compra

Para que a data da compra seja mostrada no endpoint, precisamos modificar o serializer de Compra para incluir o campo data.

from rest_framework.serializers import (
    CharField,
    CurrentUserDefault,
    DateTimeField, # novo
    HiddenField,
    ModelSerializer,
    SerializerMethodField,
    ValidationError,
)
...
class CompraSerializer(ModelSerializer):
    usuario = CharField(source="usuario.email", read_only=True)
    status = CharField(source="get_status_display", read_only=True)
    data = DateTimeField(read_only=True) # novo campo
    itens = ItensCompraSerializer(many=True, read_only=True)

    class Meta:
        model = Compra
        fields = ("id", "usuario", "status", "total", "data", "itens") # modificado
...

34. Inclusão do tipo de pagamento à entidade de Compra

Vamos adicionar o tipo de pagamento à compra. O tipo de pagamento pode ser cartão de crédito, cartão de débito, pix, boleto, transferência bancária, dinheiro ou outro.

...
class Compra(models.Model):
    class TipoPagamento(models.IntegerChoices):
        CARTAO_CREDITO = 1, "Cartão de Crédito"
        CARTAO_DEBITO = 2, "Cartão de Débito"
        PIX = 3, "PIX"
        BOLETO = 4, "Boleto"
        TRANSFERENCIA_BANCARIA = 5, "Transferência Bancária"
        DINHEIRO = 6, "Dinheiro"
        OUTRO = 7, "Outro"
...
    tipo_pagamento = models.IntegerField(choices=TipoPagamento.choices, default=TipoPagamento.CARTAO_CREDITO)
...

O campo tipo_pagamento é um campo do tipo IntegerField, que armazena o tipo de pagamento da compra. O parâmetro choices indica as opções de pagamento. O parâmetro default indica o tipo de pagamento padrão.

...
class CompraSerializer(ModelSerializer):
    usuario = CharField(source="usuario.email", read_only=True)
    status = CharField(source="get_status_display", read_only=True)
    data = DateTimeField(read_only=True)
    tipo_pagamento = CharField(source="get_tipo_pagamento_display", read_only=True) # novo campo
    itens = ItensCompraSerializer(many=True, read_only=True)

    class Meta:
        model = Compra
        fields = ("id", "usuario", "status", "total", "data", "tipo_pagamento", "itens") # modificado
...

O campo tipo_pagamento é um campo do tipo CharField, que mostra o tipo de pagamento da compra. O parâmetro source indica o método que retorna o tipo de pagamento.

O método get_tipo_pagamento_display é um método especial do model que retorna o valor legível do campo tipo_pagamento.

O campo tipo_pagamento foi incluído no atributo fields do serializer.

35. Inclusão de ações personalizadas

No Django REST Framework (DRF), ações personalizadas são endpoints adicionais que você pode criar em uma viewset usando o decorador @action. Elas permitem que você estenda as funcionalidades das viewsets além dos métodos RESTful padrão, como list, retrieve, create, update e destroy. Essas ações são úteis para operações específicas que não se encaixam perfeitamente nas operações CRUD tradicionais.

Como funcionam as ações personalizadas

Ações personalizadas são métodos definidos dentro de uma viewset e decorados com @action, que define o comportamento específico do endpoint, incluindo o verbo HTTP que será utilizado e se a ação é aplicada a um recurso específico ou a uma coleção.

Alterando o preço de um livro

Criando um serializer específico para a ação

É uma boa prática usar um serializer específico na action ajustar_preco. Isso traria várias vantagens, como validação mais robusta dos dados de entrada e organização do código. Ao usar um serializer dedicado, você garante que a lógica de validação e conversão dos dados está separada da view, seguindo o princípio de responsabilidade única e tornando o código mais limpo e reutilizável.

Vamos incluir um novo serializer chamado AjustarPrecoSerializer no arquivo serializers/livro.py:

from rest_framework.serializers import (
    DecimalField,
    ModelSerializer,
    Serializer,
    SlugRelatedField,
    ValidationError,
)
...
class LivroAlterarPrecoSerializer(Serializer):
    preco = DecimalField(max_digits=10, decimal_places=2)

    def validate_preco(self, value):
        """Valida se o preço é um valor positivo."""
        if value <= 0:
            raise ValidationError("O preço deve ser um valor positivo.")
        return value
...
...
from .livro import (
    LivroAlterarPrecoSerializer,
    LivroListSerializer,
    LivroRetrieveSerializer,
    LivroSerializer,
)
...

Criando uma ação personalizada para alterar o preço de um livro

Vamos agora criar uma ação personalizada para alterar o preço de um livro. Essa ação será aplicada a um recurso específico, ou seja, a um livro específico.

from rest_framework import status
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from core.models import Livro
from core.serializers import (
    LivroAlterarPrecoSerializer,
    LivroListSerializer,
    LivroRetrieveSerializer,
    LivroSerializer,
)
...
    @action(detail=True, methods=["patch"])
    def alterar_preco(self, request, pk=None):
        livro = self.get_object()

        serializer = LivroAlterarPrecoSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        livro.preco = serializer.validated_data["preco"]
        livro.save()

        return Response(
            {"detail": f"Preço do livro '{livro.titulo}' atualizado para {livro.preco}."}, status=status.HTTP_200_OK
        )

O decorador @action cria um endpoint para a ação alterar_preco, no formato api/livros/{id}/alterar_preco.

O método alterar_preco é um método de ação que altera o preço de um livro. Ele recebe o livro que está sendo alterado.

O método get_object() é um método que recupera um objeto com base no pk fornecido.

O método LivroAlterarPrecoSerializer é um serializer específico para a ação alterar_preco. Ele valida o preço fornecido.

O método is_valid(raise_exception=True) é um método que valida os dados fornecidos. Se os dados não forem válidos, ele lança uma exceção.

O método validated_data é um atributo que contém os dados validados.

O método Response retorna uma resposta HTTP.

O status HTTP_200_OK indica que a requisição foi bem sucedida.

Ajustando o estoque de um livro

Criando um serializer específico para a ação

Vamos incluir um novo serializer chamado LivroAjustarEstoqueSerializer no arquivo serializers/livro.py:

...
class LivroAjustarEstoqueSerializer(Serializer):
    quantidade = IntegerField()

    def validate_quantidade(self, value):
        livro = self.context.get("livro")
        if livro:
            nova_quantidade = livro.quantidade + value
            if nova_quantidade < 0:
                raise ValidationError("A quantidade em estoque não pode ser negativa.")
        return value
...
...
from .livro import (
    LivroAjustarEstoqueSerializer, # novo
    LivroAlterarPrecoSerializer,
    LivroListSerializer,
    LivroRetrieveSerializer,
    LivroSerializer,
)
...

Criando uma ação personalizada para ajustar o estoque de um livro

Vamos criar uma ação personalizada para ajustar o estoque de um livro. Essa ação será aplicada a um recurso específico, ou seja, a um livro específico.

     @action(detail=True, methods=["post"])
    def ajustar_estoque(self, request, pk=None):
        livro = self.get_object()

        serializer = LivroAjustarEstoqueSerializer(data=request.data, context={"livro": livro})
        serializer.is_valid(raise_exception=True)

        quantidade_ajuste = serializer.validated_data["quantidade"]

        livro.quantidade += quantidade_ajuste
        livro.save()

        return Response(
            {"status": "Quantidade ajustada com sucesso", "novo_estoque": livro.quantidade}, status=status.HTTP_200_OK
        )

O decorador @action cria um endpoint para a ação ajustar_estoque, no formato api/livros/{id}/ajustar_estoque.

Finalizando a compra e atualizando a quantidade de itens em estoque

Nesse momento, a compra é criada com o status CARRINHO. Vamos criar um endpoint para finalizar a compra, alterando o status da compra para FINALIZADO. No momento que a compra é finalizada, a quantidade de itens em estoque deve ser atualizada, isto é, a quantidade de itens em estoque deve ser reduzida pela quantidade de itens comprados.

from django.db import transaction

from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
...
class CompraViewSet(ModelViewSet):
...
    @action(detail=True, methods=["post"])
    def finalizar(self, request, pk=None):
        compra = self.get_object()

        if compra.status != Compra.StatusCompra.CARRINHO:
            return Response(
                status=status.HTTP_400_BAD_REQUEST,
                data={"status": "Compra já finalizada"},
            )

        with transaction.atomic():
            for item in compra.itens.all():

                if item.quantidade > item.livro.quantidade:
                    return Response(
                        status=status.HTTP_400_BAD_REQUEST,
                        data={
                            "status": "Quantidade insuficiente",
                            "livro": item.livro.titulo,
                            "quantidade_disponivel": item.livro.quantidade,
                        },
                    )

                item.livro.quantidade -= item.quantidade
                item.livro.save()

            compra.status = Compra.StatusCompra.FINALIZADO
            compra.save()

        return Response(status=status.HTTP_200_OK, data={"status": "Compra finalizada"})

O decorador @action cria um endpoint para a ação finalizar, no formato api/compras/{id}/finalizar.

O método finalizar é um método de ação que finaliza a compra. Ele recebe a compra que está sendo finalizada.

Se a compra já foi finalizada, retorna um erro.

Se a quantidade de itens em estoque for menor do que a quantidade de itens comprados, retorna um erro.

Se a quantidade de itens em estoque for maior ou igual à quantidade de itens comprados, atualiza a quantidade de itens em estoque e finaliza a compra.

O comando with transaction.atomic() garante que todas as operações dentro do bloco with sejam executadas ou nenhuma seja executada.

O método save é chamado para salvar a compra e o livro.

O método Response retorna uma resposta HTTP.

O status HTTP_200_OK indica que a requisição foi bem sucedida.

O status HTTP_400_BAD_REQUEST indica que a requisição não foi bem sucedida.

Gerando um relatório de vendas do mês

Vamos criar uma ação personalizada para gerar um relatório de vendas do mês. Essa ação será aplicada a uma coleção, ou seja, a todas as compras.

    @action(detail=False, methods=["get"])
    def relatorio_vendas_mes(self, request):
        agora = timezone.now()
        inicio_mes = agora.replace(day=1, hour=0, minute=0, second=0, microsecond=0)

        compras = Compra.objects.filter(status=Compra.StatusCompra.FINALIZADO, data__gte=inicio_mes)

        total_vendas = sum(compra.total for compra in compras)
        quantidade_vendas = compras.count()

        return Response(
            {
                "status": "Relatório de vendas deste mês",
                "total_vendas": total_vendas,
                "quantidade_vendas": quantidade_vendas,
            },
            status=status.HTTP_200_OK,
        )

O decorador @action cria um endpoint para a ação relatorio_vendas_mes, no formato api/compras/relatorio_vendas_mes.

O método relatorio_vendas_mes é um método de ação que gera um relatório de vendas do mês.

O método timezone.now() retorna a data e hora atuais.

Listando os livros com mais de 10 cópias vendidas

Vamos criar uma ação personalizada para listar os livros com mais de 10 cópias vendidas. Essa ação será aplicada a uma coleção, ou seja, a todos os livros.

Para listar os livros que possuem mais de 10 unidades vendidas, você pode usar annotate para calcular o total de unidades vendidas com base no relacionamento entre os livros e os itens de compra.

A primeira coisa que precisamos fazer é incluir um related_name no campo livro da entidade ItensCompra, em models/compra.py:

...
class ItensCompra(models.Model):
...
    livro = models.ForeignKey(Livro, on_delete=models.PROTECT, related_name="itens_compra")
...
from django.db.models.aggregates import Sum
...
    @action(detail=False, methods=["get"])
    def mais_vendidos(self, request):
        livros = Livro.objects.annotate(total_vendidos=Sum("itenscompra__quantidade")).filter(total_vendidos__gt=10)

        data = [
            {
                "id": livro.id,
                "titulo": livro.titulo,
                "total_vendidos": livro.total_vendidos,
            }
            for livro in livros
        ]

        return Response(data, status=status.HTTP_200_OK)

O decorador @action cria um endpoint para a ação mais_vendidos, no formato api/livros/mais_vendidos.

Utilizamos o método annotate com Sum para somar a quantidade de cada livro vendido, usando o relacionamento com a tabela ItensCompra (itenscompra sendo o related_name definido na model).

A filtragem é feita com filter(total_vendidos__gt=10) para incluir apenas livros que tenham mais de 10 unidades vendidas.

Os dados retornados são compostos pelo id e título do livro (livro.id e livro.titulo) e pelo total de unidades vendidas (livro.total_vendidos), obtido pelo annotate.

Ao fazer uma solicitação GET para o endpoint /api/livros/mais_vendidos/, a resposta será algo assim:

[
  {
    "id": 1,
    "titulo": "O Código Limpo",
    "total_vendidos": 33
  },
  {
    "id": 2,
    "titulo": "O Codificador Limpo",
    "total_vendidos": 25
  },
]

36. Utilização de filtros

Nesse momento, é possível apenas listar todos os livros. Vamos ver como podemos filtrar os livros por seus atributos, como categoria, editora e autores.

Para isso, vamos utilizar o pacote django-filter, que nos permite filtrar os resultados de uma consulta. Ele já está instalado no projeto.

Filtrando os livros por categoria

Vamos começar filtrando os livros por categoria.

...
from django_filters.rest_framework import DjangoFilterBackend
...
class LivroViewSet(viewsets.ModelViewSet):
    queryset = Livro.objects.all()
    serializer_class = LivroSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ["categoria__descricao"]
...

O DjangoFilterBackend é o filtro do django-filter.

O filterset_fields indica quais campos serão filtrados. Nesse caso, estamos filtrando apenas pelo campo categoria__descricao.

Acrescentando outros filtros na listagem de livros

Vamos acrescentar outros filtros na listagem de livros.

...
    filterset_fields = ["categoria__descricao", "editora__nome"]
...

O filterset_fields indica quais campos serão filtrados. Nesse caso, estamos filtrando pelos campos categoria__descricao e editora__nome.

Da mesma forma, por outros campos.

Exercício

37. Utilização de busca textual

A busca textual serve para adicionar a funcionalidade de realizar buscas dentro de determinados valores de texto armazenados na base de dados.

Contudo a busca só funciona para campos de texto, como CharField e TextField.

...
from rest_framework.filters import SearchFilter
...

class LivroViewSet(viewsets.ModelViewSet):
...
    filter_backends = [DjangoFilterBackend, SearchFilter]
    filterset_fields = ["categoria__descricao", "editora__nome"]
    search_fields = ["titulo"]
...

Exercício

38. Utilização de ordenação dos resultados

Toda viewset possui um atributo chamado ordering_fields, que é uma lista de campos que podem ser utilizados para ordenar os resultados. Além disso, o atributo ordering é utilizado para definir o campo padrão de ordenação. Se você ainda quiser permitir a ordenação reversa, basta adicionar um sinal de menos (-) na frente do campo.

Independentemente dessa ordenação padrão, o usuário pode ordenar os resultados de acordo com o campo desejado, passando o nome do campo como parâmetro na URL.

A ordenação serve para adicionar a funcionalidade de ordenar os resultados de uma consulta.

...
from rest_framework.filters import SearchFilter, OrderingFilter
...

class LivroViewSet(viewsets.ModelViewSet):
...
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ["categoria__descricao", "editora__nome"]
    search_fields = ["titulo"]
    ordering_fields = ["titulo", "preco"]
    ordering = ["titulo"]
...

Esses são apenas alguns exemplos de como utilizar os filtros, a pesquisa textual e a ordenação. Você pode combinar esses recursos da forma que desejar.

Acrescentando filtro e ordenação por data

Vamos ver ainda um último exemplo de como adicionar filtro e ordenação.

...
    filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
    filterset_fields = ["usuario__email", "status", "data"]
    search_fields = ["usuario__email"]
    ordering_fields = ["usuario__email", "status", "data"]
    ordering = ["-data"]
...

Exercício


39. Inclusão do limite de um carrinho de compras por usuário

Nesse momento, um usuário pode ter vários carrinhos de compras. Vamos limitar a um carrinho de compras por usuário. Faremos isso verificando se o usuário já possui um carrinho de compras. Se ele já tiver, retornaremos o carrinho existente. Caso contrário, criaremos um novo carrinho. Vamos aproveitar e verificar se um livro já foi adicionado ao carrinho. Se ele já foi adicionado, vamos incrementar a quantidade.

Uma vantagem dessa abordagem é que podemos incluir um livro no carrinho simplesmente enviando o id do livro e a quantidade. Se o livro já estiver no carrinho, a quantidade será incrementada. Se o livro não estiver no carrinho, ele será adicionado.

class CompraCreateUpdateSerializer(ModelSerializer):
    usuario = HiddenField(default=CurrentUserDefault())
    itens = ItensCompraCreateUpdateSerializer(many=True)

    class Meta:
        model = Compra
        fields = ("usuario", "itens")

    def create(self, validated_data):
        itens = validated_data.pop("itens")
        usuario = validated_data["usuario"]

        compra, criada = Compra.objects.get_or_create(
            usuario=usuario, status=Compra.StatusCompra.CARRINHO, defaults=validated_data
        )

        for item in itens:
            item_existente = compra.itens.filter(livro=item["livro"]).first()

            if item_existente:
                item_existente.quantidade += item["quantidade"]
                item_existente.preco = item["livro"].preco
                item_existente.save()
            else:
                item["preco"] = item["livro"].preco
                ItensCompra.objects.create(compra=compra, **item)

        return compra

    def update(self, compra, validated_data):
        itens = validated_data.pop("itens", [])
        if itens:
            compra.itens.all().delete()
            for item in itens:
                item["preco"] = item["livro"].preco
                ItensCompra.objects.create(compra=compra, **item)

        return super().update(compra, validated_data)

O método get_or_create retorna um objeto Compra existente ou cria um novo objeto Compra se ele não existir.

O método filter retorna um objeto ItensCompra que atenda aos critérios de pesquisa.

O método first retorna o primeiro objeto ItensCompra que atenda aos critérios de pesquisa ou None se não houver objetos.

40. Inclusão do total da compra na model de compra

Adicionar um campo total ao modelo de Compra para armazenar o valor total é uma solução eficaz em termos de performance e facilidade de uso em consultas frequentes. Com isso, o valor total será calculado e armazenado diretamente no banco de dados, permitindo que você ordene ou filtre pelas compras com eficiência.

class Compra(models.Model):
    ...
    total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    ...

Vamos também incluir um método save para calcular o total da compra:

class Compra(models.Model):
    ...
    def save(self, *args, **kwargs):
        self.total = sum(item.preco * item.quantidade for item in self.itens.all())
        super().save(*args, **kwargs)
    ...

O método save é um método especial que é chamado sempre que um objeto é salvo no banco de dados.

O método super().save(*args, **kwargs) chama o método save da classe pai.

O método sum retorna a soma de todos os valores em um iterável.

O método self.itens.all() retorna todos os itens da compra.

Podemos retirar a property total da classe Compra:

class Compra(models.Model):
    ...
    @property
    def total(self):
        return sum(item.preco * item.quantidade for item in self.itens.all())
    ...

Precisamos ainda incluir o salvamento da compra no método create do serializer CompraCreateUpdateSerializer:

class CompraCreateUpdateSerializer(ModelSerializer):
    ...
    def create(self, validated_data):
        ...
        compra.save() # linha adicionada para salvar a compra
        return compra
    ...

O método save é chamado para salvar a compr, atualizando assim o campo total.

for compra in Compra.objects.all():
    compra.save()

Ordenações e consultas

Após adicionar o campo total, você pode usá-lo diretamente no shell do Django em consultas para ordenar ou filtrar as compras.

compras = Compra.objects.all().order_by("-total")
compras = Compra.objects.filter(total__gte=100)

Exercícios Garagem

O projeto Garagem é um projeto de uma garagem de carros. O objetivo é praticar aquilo que foi visto nesse tutorial, no projeto core.

E1. Crie o projeto Garagem

Seguindo aquilo que você já aprendeu na criação do projeto da Livraria, crie um novo projeto, a partir do template.

  1. O projeto será chamado Garagem.
  2. Nomeie o commit como sendo Criação do projeto.
  3. Siga esses passos para criar a API.
  4. Crie as seguintes APIs, fazendo um commit para cada uma:
    • Acessorio:
      • descricao (string, máximo 100 caracteres).
      • __str__ (retorna a descrição e o id).
      • Exemplos: Ar condicionado, Direção hidráulica, Vidros elétricos, Travas elétricas, Alarme, Airbag, Freios ABS.
    • Categoria:
      • descricao (string, máximo 100 caracteres).
      • __str__ (retorna a descrição e o id.
      • Exemplos: Sedan, Hatch, SUV, Picape, Caminhonete, Conversível, Esportivo, Utilitário.
    • Cor:
      • nome (string, máximo 100 caracteres).
      • __str__ (retorna o nome e o id).
      • Exemplo: Preto, Branco, Prata, Vermelho, Cinza, Grafite.
    • Marca:
      • nome (string, máximo 50 caracteres).
      • nacionalidade (string, máximo 50 caracteres, permite nulo).
      • __str__ (retorna o nome em caixa alta e o id).
      • Exemplo: FORD, CHEVROLET, VOLKSWAGEN, FIAT, RENAULT, TOYOTA, HONDA, HYUNDAI, KIA, NISSAN, PEUGEOT, CITROEN, JEEP, MITSUBISHI, MERCEDES-BENZ, BMW, AUDI, VOLVO.
  5. Crie a aplicação frontend com Vuejs para consumir a API REST do projeto Garagem. Pode utilizar o template do projeto da livraria-vue3 como base.

E2. Crie o modelo Modelo

Vamos incluir o modelo Modelo no projeto Garagem.

E3. Crie o modelo Veiculo

Vamos incluir o modelo Veiculo no projeto Garagem.


Apêndices

A1. Instalação e atualização do VS Code

Para instalar ou atualizar o VS Code, siga as seguintes instruções:

No Ubuntu/Mint e derivados:

sudo apt install code

No Manjaro:

yay -Syu visual-studio-code-bin

No Windows:

A2. Instalação e sincronização de extensões do VS Code

Instalação de extensões no VS Code

Instale as extensoẽs do VS Code de sua preferência. Você pode instalar as extensões clicando no ícone de extensões no canto esquerdo da tela do VS Code e pesquisando pelo nome da extensão.

Eu recomendo as seguintes:

Extensão Vue.js devtools no Google Chrome

Sinconização de extensões no VS Code

Você pode configurar a sincronização das extensões entre os computadores. Para isso:

A3. Instalação e configuração do PDM

Instalação do PDM no Linux

As instruções a seguir são para o Linux Manjaro e Ubuntu. Se você estiver usando outra distribuição ou quiser mais informações, consulte a documentação do PDM.

pdm -V
curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -

IMPORTANTE: Após a instalação do PDM, você precisa rodar o script de configuração, conforme descrito abaixo.

Configuração do PDM no bash (Ubuntu e derivados)

curl -sSL https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/pdm_config_bash.sh | bash

Configuração do PDM no zsh com o Oh! My Zsh (Manjaro e derivados)

curl -sSL https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/pdm_config_ohmyzsh.sh | zsh

Instalação do PDM no Windows

Execute o comando abaixo no PowerShell (pode ser no Terminal do VS Code):

(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -

Verifique se o PDM está configurado para não usar virtualenv:

pdm config

IMPORTANTE: Se você não fizer essa configuração, o PDM irá criar uma pasta .venv no diretório do projeto. Para resolver isso, você deve apagar a pasta .venv e executar o comando pdm config python.use_venv false e então executar o comando pdm install.

Se precisar instalar o Python:

sudo apt install python-is-python3 python3.10-venv

Voltar para a preparação do ambiente

A4. Criação do Banco de Dados no Supabase

Para evitar a perda dos dados a cada nova publicação do projeto, vamos criar um banco de dados externamente no Supabase. O banco de dados SQLite local será utilizado apenas para desenvolvimento.

Criando um projeto no Supabase

Para criar o banco de dados no Supabase, siga as instruções a seguir:

Configurando o banco de dados no projeto

# Supabase
DATABASE_URL=postgres://postgres:teste.123@!@db.vqcprcexhnwvyvewgrin.supabase.co:5432/postgres

Migrando o banco de dados

pdm run migrate

Observe que o banco de dados foi migrado no Supabase.

Para testar, crie alguns registros no banco de dados. Depois volte a configuração local e perceba que os dados são diferentes na base local e na base do Supabase.

Carregando os dados iniciais

Utilizando o banco de dados local

Após fazer as alterações no banco de dados remoto, volte a configuração para utilizar o banco de dados local:

IMPORTANTE: A cada nova alteração no banco de dados, você deve repetir esse processo de migração, tanto no banco de dados local quanto no banco de dados do Supabase.

A5. Publicação do projeto no Render

O Render é uma plataforma de hospedagem que permite publicar aplicações web, bancos de dados e outros serviços. No site existe um link para o tutorial oficial: https://render.com/docs/deploy-django

Criando um script de Build

Precisamos executar uma série de comandos para construir nosso aplicativo. Podemos fazer isso com um script de construção (build script).

#!/usr/bin/env bash
# Sai do script se houver algum erro
set -o errexit

# Atualiza o pip
pip install --upgrade pip

# Instala as dependências
pip install -r requirements.txt

# Coleta os arquivos estáticos
python manage.py collectstatic --no-input

# Aplica as migrações
python manage.py migrate
chmod a+x build.sh
pdm add uvicorn gunicorn

Testando a execução localmente

pdm run python -m gunicorn app.asgi:application -k uvicorn.workers.UvicornWorker

O que fizemos foi substituir o servidor de desenvolvimento do Django pelo servidor Uvicorn e Gunicorn.

Configurando o Render

MODE=PRODUCTION
DEBUG=False
SECRET_KEY=[sua_secret_key]
WEB_CONCURRENCY=4
DATABASE_URL=[sua_database_url]
CLOUDINARY_URL=cloudinary://your_api_key:your_api_secret@your_cloud_name
PASSAGE_APP_ID=sua_app_id
PASSAGE_API_KEY=sua_api_key

Crie uma SECRET_KEY nova. Veja como aqui. Coloque essa chave no lugar de [sua_secret_key].

Coloque a URL do banco de dados do Supabase no lugar de [sua_database_url].

Se tudo estiver correto, o projeto será implantado no Render.

A6. Armazenanamento de arquivos estáticos no Cloudinary

Vamos utilizar o Cloudinary para armazenar os arquivos estáticos, como as imagens dos livros. Desta forma, os arquivos não serão perdidos a cada nova implantação.

Criando uma conta no Cloudinary

Configurando o Cloudinary

# Cloudinary
CLOUDINARY_URL=cloudinary://your_api_key:your_api_secret@your_cloud_name

Altere as informações de acordo com o seu projeto, acessando o Cloudinary Console na opção Dashboard.

Testando

A7. Resolução de erros

Liberando uma porta em uso

pdm run python manage.py runserver
Error: That port is already in use.
fuser -k 19003/tcp

Este comando vai matar o processo que está rodando na porta 19003. Mude o número da porta conforme necessário.

Removendo temporários, migrations e o banco de dados

find . -name "__pycache__" -type d -exec rm -r {} +
find . -path "*/migrations/*.pyc" -delete
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete
rm -rf __pypackages__ pdm.lock
rm db.sqlite3

Pasta .venv criada no projeto

rm -rf .venv
pdm config python.use_venv false

Geração da SECRET_KEY

A SECRET_KEY é uma chave secreta usada pelo Django para criptografar dados sensíveis. Ela é usada, por exemplo, para criptografar as senhas dos usuários. Em sistemas em produção ela deve ser mantida em segredo.

python -c "import secrets; print(secrets.token_urlsafe())"
pdm run python manage.py shell -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

Para saber mais sobre a chave secreta, acesse a documentação do Django.

Não esqueça de substituir a chave secreta pelo valor gerado.

Abrindo um arquivo sqlite3 na web

Aumentando o tempo de vida do token de autenticação JWT

from datetime import timedelta
...
SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=180),
    "REFRESH_TOKEN_LIFETIME":timedelta(days=1),
}

A8. Configuração do git

Um aviso importante

Antes de mais nada, seguem 3 regras a serem consideradas ao seguir as instruções:

As 3 regras falam a mesma coisa? Sim, você entendeu o recado. ;-)

Configurando o projeto git

rm -Rf ~/.git
Control + Shift + P + "Recarregar a Janela"

Configurando as variáveis do git

git config --global user.name "Seu Nome"
git config --global user.email "seuEmailNoGitHub@gmail.com"
git config -l
rm ~/.gitconfig

Repita o processo de configuração de nome e email.

A9. Uso do curl para testar a API via linha de comando

curl -X GET http://0.0.0.0:19003/api/categorias/
curl -X GET http://0.0.0.0:19003/api/categorias/1/
curl -X POST http://0.0.0.0:19003/api/categorias/ -d "descricao=Teste"
curl -X PUT http://0.0.0.0:19003/api/categorias/1/ -d "descricao=Teste 2"
curl -X DELETE http://0.0.0.0:19003/api/categorias/1/

A10. Django Shell - Comandos úteis

Seguem abaixo alguns comandos úteis para serem executados no Django Shell:

from core.models import Categoria
c = Categoria(descricao="Teste")
c.save()
Categoria.objects.all()
Categoria.objects.get(id=1)
c = Categoria.objects.get(id=1)
c.descricao = "Teste 2"
c.save()
c = Categoria.objects.get(id=1)
c.delete()
from core.models import Livro
Livro.objects.filter(preco=10)
Livro.objects.filter(preco=0).count()

ou

len(Livro.objects.filter(preco=0))
Livro.objects.filter(preco=0).update(preco=10)
Livro.objects.filter(preco__isnull=True)
for livro in Editora.objects.get(id=167).livros.all():
    livro.editora_id = 11
    livro.save()
Categoria.objects.get(descricao="Comédia").livros.all()
Livro.objects.filter(categoria__descricao="Comédia")
for categoria in Categoria.objects.all():
    if len(categoria.livros.all()) == 0:
        print(categoria)
        categoria.delete()

A11. DBShell - Comandos úteis

Antes de utilizar o DBShell, é necessário instalar o pacote sqlite3.

Ubuntu/Mint e derivados:

sudo apt install sqlite3

Manjaro:

sudo pacman -S sqlite3

Seguem abaixo alguns comandos úteis para serem executados no DBShell:

DELETE FROM core_categoria;
DELETE FROM core_user WHERE id > 1;
UPDATE core_livro SET preco = 10 WHERE preco IS NULL;
UPDATE core_livro SET preco = 10 WHERE preco = 0;
SELECT * FROM core_livro WHERE preco = 0;
SELECT * FROM core_livro WHERE preco IS NULL;
SELECT * FROM core_livro WHERE categoria_id = 1;

Contribua

Para contriburi com esse projeto:


Marco André Mendes <marcoandre@gmail.com>