DJANGO COM DRF (2025)
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.
Existe uma versão completa e funcional do projeto da livraria, que pode ser acessada neste repositório do GitHub e está publicada no render.
Este tutorial está em constante desenvolvimento. Envie sugestões e correções para meu e-mail. 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!
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.1 O projeto Livraria
Este projeto consiste em uma API REST para uma livraria. Ele terá as seguintes classes:
Categoria: representa a categoria de um livro.Editora: representa a editora de um livro.Autor: representa o autor de um livro.Livro: representa um livro.User: representa um usuário do sistema.Compra: representa uma compra de livros.ItemCompra: representa um item de uma compra.Modelo Entidade Relacionamento
O modelo entidade relacionamento (MER) do projeto é o seguinte:

Diagrama de Classes
O diagrama de classes do projeto é o seguinte:

Modelo de Dados do Django
O modelo de dados do Django é o seguinte:

2.2 Criação do projeto a partir de um template
IMPORTANTE: Vamos criar o projeto
livrariaa partir de um repositório de template. Se você quiser criar aprender a criar um projeto do zero, acesse o tutorial de 2023.
Use this template em Create a new repository.Owner: Repository name: livrariaCreate repository.Feito isso, o repositório
livrariaserá criado no seu GitHub.
2.3 Clonando o projeto
Você pode clonar o projeto de duas formas:
2.3.1 Usando o VS Code
Clone Repository.Control+Shift+P e digitar Clone Repository.Clone.2.3.2 Usando o terminal
git clone <URL do repositório>
code .
O projeto criado ficará assim:

2.4 Instalando as dependências
pdm install
2.5 Criando o arquivo .env
.env, a partir do arquivo .env.exemplo:.env.exemplo.Salvar como... (Ctrl+Shift+S)..env.Opcionalmente, você pode criar o arquivo
.enva partir do terminal, digitando:
cp .env.exemplo .env
2.4 Rodando o servidor de desenvolvimento
pdm run dev
2.5 Acessando o projeto
Acesse o projeto no navegador:
http://0.0.0.0:19003/admin
a@a.comteste.123IMPORTANTE: O servidor de desenvolvimento deve estar sempre rodando para que o projeto funcione.
É isso! Seu projeto está inicializado e rodando!!!
2.6 Exercício
Admin está em execuçã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:

Todas as aplicações precisam ser adicionadas ao arquivo
settings.pydo projeto, na seçãoINSTALLED_APPS.
Dentro da pasta core temos alguns arquivos e pastas, mas os mais importantes são:
migrations: é a pasta de migrações de banco de dados da aplicação.models: é a pasta onde ficam as models (modelos de banco de dados, ou tabelas) da aplicação.serializers: é a pasta onde ficam os serializadores (serializadores) da aplicação.views: é a pasta onde ficam as views (visões) da aplicação.admin.py: é o arquivo de configuração do Admin, uma ferramenta que permite que você gerencie os dados do seu site.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
modelsjá contém um modelo de dados, dentro do arquivouser.py, chamadoUser. 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
Vamos começar criando o modelo de dados Categoria, que representa uma categoria de livro, como por exemplo: Ficção, Terror, Romance, etc.
models da aplicação core crie um arquivo chamado categoria.py.categoria.py:from django.db import models
class Categoria(models.Model):
descricao = models.CharField(max_length=100)
Nesse código, você:
model;Categoria;Incluiu o campo descricao, que é uma string de no máximo 100 caracteres. Esse campo é obrigatório.
3.4 Inclusão da model no arquivo __init__.py
model no arquivo __init__.py da pasta models:from .categoria import Categoria
3.5 Efetivando a criação da tabela
Precisamos ainda efetivar a criação da tabela no banco de dados.
Abra um novo terminal, deixando o terminal antigo executando o servidor do projeto.
Crie as migrações:
pdm run migrate
Esse comando executará 3 comandos em sequência:
makemigrations: cria as migrações de banco de dados.migrate: efetiva as migrações no banco de dados.graph_models: cria/atualiza um diagrama de classes do modelo de dados.
db.sqlite3) e verifique se a tabela core_categoria foi criada.core.png na pasta raiz do projeto.Admin do projeto e verifique se a nova tabela aparece lá.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.
model no Admin. Abra o arquivo admin.py da aplicação core e adicione o seguinte código no final do arquivo:admin.site.register(models.Categoria)
3.7 Exercício
Admin e inclua algumas categorias no banco de dados.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
Categoria object (1) e assim por diante.model Categoria.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.
__str__ na model Categoria:...
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 umastring.
Volte ao Admin verifique o que mudou na apresentação dos objetos da model Categoria.
3.11 Hora de fazer um commit
feat: criação da model de Categoria
IMPORTANTE: Escrevendo uma boa mensagem de commit
Alteração 1, Alteração 2, Alteração 3, etc.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)
DRF já está instalado no projeto, conforme os arquivos pyproject.toml e requirements.txt.DRF já está configurado no arquivo settings.py, na seção INSTALLED_APPS.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
Serializer (ou serializador, em português) é uma classe que transforma objetos Python (como modelos) em formatos que podem ser enviados pela internet (como JSON), e vice-versa.”
categoria.py na pasta serializers da aplicação core, e adicione o seguinte código, para criar a CategoriaSerializer: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
model = Categoria: define o model que será serializado.fields = '__all__': define que todos os campos serão serializados.4.2.2 Inclusão do serializer no __init__.py
__init__.py da pasta serializers: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.
CategoriaViewSet na pasta views da aplicação core, no arquivo categoria.py: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
queryset = Categoria.objects.all(): define o conjunto de objetos que será retornado pela view.serializer_class = CategoriaSerializer: define o serializer que será utilizado para serializar os objetos.4.3.2 Inclusão da view no __init__.py
__init__.py da pasta views:from .categoria import CategoriaViewSet
4.4 Criação das rotas (urls)
As rotas são responsáveis por mapear as URLs para as views.
Categoria, edite o arquivo urls.py na pasta app e adicione as linhas indicadas:...
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
Para acessar a interface gerada pelo DRF, acesse:
http://0.0.0.0:19003/api/
Se tudo correu bem, você deve ver a interface do DRF.
Categoria:
http://0.0.0.0:19003/api/categorias/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.
Para acessar o Swagger, acesse:
http://0.0.0.0:19003/api/swagger/
4.9 Exercícios: testando a API e as ferramentas
Instale uma ou mais das ferramentas sugeridas.
4.10 Fazendo um commit
feat: criação da API para Categoria
Agora que temos uma API REST completa, vamos criar uma aplicação frontend em Vuejs para consumir essa API da Categoria.
Use this template em Create a new repository. 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.
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
Editora são os mesmos que fizemos para a classe Categoria:
model Editora no arquivo editora.py na pasta models.model no arquivo __init__.py da pasta models.model no arquivo admin.py.editora.pyna pasta serializers.__init__.py da pasta serializers.editora.pyna pasta views.viewset no arquivo __init__.py da pasta views.urls.py.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 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 Editora
from core.serializers import EditoraSerializer
...
class EditoraViewSet(ModelViewSet):
queryset = Editora.objects.all()
serializer_class = EditoraSerializer
views/__init__.py
...
from .editora import EditoraViewSet
urls.py
...
from core.views import CategoriaViewSet, EditoraViewSet, UserViewSet
...
router.register(r'categorias', CategoriaViewSet)
router.register(r'editoras', EditoraViewSet)
...
6.3 Fazendo a migração e efetivando a migração
pdm run migrate
core_editora foi criada no banco de dados.6.4 Exercícios: testando da API da Editora
Editora.6.5 Fazendo um commit
feat: criação da API para Editora
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.
Autor.O autor terá os seguintes atributos:
nome: string de no máximo 100 caracteres.email: campo do tipo e-mail de no máximo 100 caracteres, que pode ser nulo.
feat: criação da API para Autor
Exercícios:
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 automática dos arquivos necessários
Para facilitar a criação dos arquivos necessários para a model Livro, utilizar um script que cria automaticamente os arquivos necessários. Além disso, ele abre todos os arquivos necessários para criar a API, na ordem correta.
Antes de executar o script, feche todas as abas do VS Code com o atalho Ctrl+K W.
Execute o seguinte comando no terminal:
pdm cria_api livro
O comando
pdm cria_api livroé um comando que executa um script Python que cria automaticamente os arquivos necessários para a modelLivro. Ele também abre todos os arquivos necessários para criar a API, na ordem correta.
8.2 Criando o modelo de dados Livro
Livro, no arquivo models/livro.py:
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)
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:

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.
Livro.feat: criação da entidade para 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
Livro, logo após o atributo preco:from .categoria import Categoria
...
categoria = models.ForeignKey(
Categoria, on_delete=models.PROTECT, related_name='livros', null=True, blank=True
)
...
models.ForeignKey: define o campo como sendo uma chave estrangeira.Categoria: o model que será associado a este campo.on_delete=models.PROTECT: impede de apagar uma categoria que possua livros associados. É conhecido integridade referencial. Outras formas de definir o comportamento são:
models.PROTECT: impede a exclusão de um objeto que possui referências em outros objetos.models.CASCADE: exclui todos os objetos associados ao objeto que está sendo excluído.models.SET_NULL: define o campo como nulo quando o objeto associado é excluído.models.SET_DEFAULT: define o campo como o valor padrão quando o objeto associado é excluído.related_name='livros': é chamado de relacionamento reverso. Cria um atributo na classe Categoria que permite acessar todos os livros de uma categoria. Ou seja, quando você acessar uma categoria, poderá acessar todos os livros associados a ela.null=True, blank=True:
null=True: permite que o campo seja nulo no banco de dados.blank=True: permite que o campo seja nulo no formulário do Django Admin.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_ideeditora_idforam criados no banco de dados, na tabelacore_livro. Eles são os campos que fazem referência às tabelascore_categoriaecore_editora.
A model Livro ficará assim:

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
id 1:>>> 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.
feat: inclusão do relacionamento de Livro com Categoria e Editora
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.).
autores no modelo Livro:from .autor import Autor
...
autores = models.ManyToManyField(Autor, related_name='livros', blank=True)
...
Observe que o campo
autoresnão foi criado na tabelacore_livro. Ao invés disso, uma tabela associativa foi criada, com o nomecore_livro_autores, contendo os camposlivro_ideautor_id. É assim que é feito um relacionamento n para n no Django.
Nesse caso, não é necessário usar o atributo
null=Trueeblank=True, pois um campo do tipoManyToManyFieldcria uma tabela associativa.
Livro ficará assim:
Note que na ligação entre
LivroeAutorexistem uma “bolinha” em cada lado, indicando que o relacionamento é n para n.
Já no caso de
LivrocomCategoriaeEditora, existe uma “bolinha” emLivroe um “pino” emCategoriaeEditora, indicando que o relacionamento é n para 1.
Observe as alterações no banco de dados, no Admin e na API.
feat: inclusão do relacionamento n para n entre Livro e Autor
10.2 Exercícios
Acesse a API do Livro e veja como está a apresentação dos autores:
http://0.0.0.0:19003/api/livros/
Observou que no
Livro, aparecem apenas os camposidda 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
LivroListRetrieveSerializer no arquivo serializers/__init__.py:from .livro import LivroListRetrieveSerializer, LivroSerializer
Observe que no
LivroListRetrieveSerializerfoi incluído o atributodepth = 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 oLivroSerializeré utilizado para as demais operações, ou seja, criação e alteração.
feat: criação de dois serializadores para Livro
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.
LivroListSerializer para a listagem de livros, que mostre apenas o id, o título e o preço e renomeie o serializador LivroListRetrieveSerializer para LivroRetrieveSerializer: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 oLivroRetrieveSerializeré utilizado na recuperação de um único livro e oLivroSerializeré utilizado nas demais operações.
serializers/__init__.py:from .livro import LivroListSerializer, LivroRetrieveSerializer, LivroSerializer
feat: criação de múltiplos serializadores para Livro
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.
Linux, execute o seguinte comando no terminal:wget https://github.com/marrcandre/django-drf-tutorial/raw/main/apps/uploader.zip -O uploader.zip && unzip uploader.zip && rm -v uploader.zip
Windows, execute os seguintes comandos no PowerShell: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:

Instalando as dependências
__pypackages__ e o arquivo pdm.lock:rm -rf __pypackages__ pdm.lock
pdm.lock:pdm lock
pdm install
Registro da app
uploader na lista de INSTALLED_APPS, no settings.py:INSTALLED_APPS = [
...
'uploader', # nova linha
'core',
...
]
IMPORTANTE: Não esqueça da vírgula no final da linha.
Configuração no urls.py
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 no arquivo pyproject.toml para incluir a geração do diagrama da app uploader:post_migrate = "python manage.py graph_models --disable-sort-fields -S -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.
models/livro.py da aplicação livraria e inclua o seguinte conteúdo:...
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 tabelauploader_image.
O atributo
related_name='+'indica que não será criado um atributo inverso na tabelauploader_image.
O atributo
on_delete=models.SET_NULLindica que, ao apagar a imagem, o campocapaserá setado comoNULL.
pdm run migrate
O modelo Livro ficará assim:

Observe que o campo
capa_idfoi criado na tabelacore_livro, fazendo referência à tabelauploader_image.
Uso no serializer
serializers/livro.py da aplicação core e inclua o seguinte conteúdo:...
from rest_framework.serializers import ModelSerializer, SlugRelatedField
from uploader.models import Image
from uploader.serializers import ImageSerializer
...
class LivroRetrieveSerializer(ModelSerializer):
capa = ImageSerializer(required=False)
class Meta:
model = Livro
fields = '__all__'
depth = 1
...
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 Meta:
model = Livro
fields = '__all__'
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 campocapaé utilizado para a recuperação da imagem.
Teste de upload e associação com o livro
Acesse a API de media:
http://0.0.0.0:19003/api/media/images/
capa_attachment_key foi preenchido com o valor attachment_key da imagem.capa_attachment_key.capa_attachment_key com o valor guardado anteriormente.Acesse o endpoint http://0.0.0.0:19003/api/media/images/ e observe que a imagem foi associada ao livro.
feat: inclusão da app de upload e associação de imagens
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
Acesse o seguinte link:
http://191.52.55.236:19003/admin/ (ou peça ao professor)a@a.comteste.123O Senhor dos Anéis - A Sociedade do AnelCópia de segurança dos dados
dumpdata:pdm run dumpdata > core.json
core_bkp.json foi criado: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:
core.json:No Linux:
wget https://raw.githubusercontent.com/marrcandre/django-drf-tutorial/refs/heads/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
loaddata:pdm run loaddata
O comando espera um arquivo
core.jsonna pasta raiz do projeto.
Criando os campos email e cidade para Editora
Você deve receber uma mensagem de erro ao tentar fazer o “load” dos dados, pois os campos email e cidade não existem na model Editora. Para resolver isso, você deve criar esses campos na model Editora.
models/editora.py e adicione os campos email e cidade:class Editora(models.Model):
...
email = models.EmailField(max_length=100, blank=True, null=True)
cidade = models.CharField(max_length=100, blank=True, null=True)
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.
O Admin é uma ferramenta para gerenciar os dados do banco de dados. Ele pode ser customizado para melhorar a experiência do usuário.
core/admin.py: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):
...
admin.site.register(User, BaseUserAdmin) deve ser removida.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
admin.site.register() devem ser removidas.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.
Acesse o Admin e veja as modificações:
http://0.0.0.0:19003/api/admin/
Faça um commit com a mensagem:
feat: customização do Admin
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()
Para mais exemplos de uso do Django Shell Plus, acesse este anexo.
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 entre as ações que compõem o CRUD, os termos utilizados no Admin, os verbos HTTP e as actions dos serializadores do Django REST Framework.:
| Ação | CRUD | Admin | Verbos HTTP | Ações do DRF |
|---|---|---|---|---|
| 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ícios
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:
administradores, com as seguintes as permissões:
autor, categoria eeditora.livro.compradores, com as seguintes permissões:
autor, categoria e editora.livro.As permissões para compradores devem ficar assim:

b. Criando usuários e adicionando aos grupos
admin1@a.com e o inclua no grupo Administradores.comprador1@a.com e o inclua no grupo Compradores.c. Testando as permissões
Admin com o usuário admin1@a.com e verifique se ele tem acesso a todas as permissões do grupo Administradores.
autor, categoria, editora.livro (mas não deve conseguir remover livro).Admin com o usuário comprador1@a.com e verifique se ele tem acesso apenas às permissões do grupo Compradores.
autor, categoria e editora, sem alterar ou excluir esses objetos.livro, mas não deve conseguir excluir livros.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:
views ou viewsets, por exemplo);settings.py;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
CategoriaViewSet:permission_classes = [IsAuthenticated]
Para testar:
"As credenciais de autenticação não foram fornecidas."Resumindo, utilizamos a classe
IsAuthenticatedpara 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
IsAuthenticatednosettings.pypara 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:
POST exigem que o usuário tenha a permissão de adição (add) no modelo.PUT e PATCH exigem que o usuário tenha a permissão de alteração (change) no modelo.DELETE exigem que o usuário tenha a permissão de exclusão (remove) no modelo.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.
Criação da conta no Passage
Se você ainda não tem uma conta no Passage:
Login e depois em Registre-se para criar uma conta. Siga os passos solicitados para criar a conta.Criação de um aplicativo no Passage
Após criar a conta, você deve criar um aplicativo:
Create App.Passkey complete e clique no botão Continue.Embedded login experience e preencha os campos solicitados:
Name your app: livraria (por exemplo)Enter the domain for your app: http://localhost:5173Enter the redirect URL: /Create App para finalizar a criação do aplicativoImportante: 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
settings.py: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
src/views/Login.vue, inclua o seguinte código: <passage-auth app-id="seu_app_id"></passage-auth>
Substitua o valor de app-id pelo valor do seu app_id, no Passage.
Vamos aproveitar a aplicação uploader para incluir a foto de perfil no usuário.
Criação do campo de foto de perfil
models/user.py, inclua o campo foto:...
from uploader.models import Image
...
class User(AbstractBaseUser, PermissionsMixin):
foto = models.ForeignKey(
Image,
related_name='user_foto',
on_delete=models.SET_NULL,
null=True,
blank=True,
default=None,
)
O campo
fotoé uma chave estrangeira para a tabelauploader_image.
A foto será opcional, por isso utilizamos
null=Trueeblank=True.
O campo
fotoseránullpor padrão, por isso utilizamosdefault=None.
Se a foto for deletada, o campo
fotoseránull, por isso utilizamoson_delete=models.SET_NULL.
Seu projeto deve ficar assim:

Observe a ligação entre a model
Usere a modelImage, através da chave estrangeirafoto.
Inclusão da foto no Admin
admin.py, inclua o campo foto:...
class UserAdmin(BaseUserAdmin):
...
(_('Personal Info'), {'fields': ('name', 'passage_id', 'foto')}),# inclua a foto aqui
...
Admin.Inclusão da foto no serializer
serializers/user.py, por este: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=Trueindica que o campofoto_attachment_keyé apenas para escrita. Isso significa que ele não será exibido na resposta da API.
O atributo
read_only=Trueindica que o campofotoé apenas para leitura. Isso significa que ele não será aceito na requisição da API.
Testando
Finalizando
feat: inclusão da foto de perfil no usuário
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
compra.py dentro da pasta models do app core, digitando no terminal:touch core/models/compra.py
compra.py recém criado:from django.db import models
from .user import User
class Compra(models.Model):
class StatusCompra(models.IntegerChoices):
CARRINHO = 1, 'Carrinho'
FINALIZADO = 2, 'Finalizado'
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
UsercomoForeignKeypara a modelCompra.
StatusCompraé do tipoIntegerChoices, que é uma forma de criar um campochoicescom valores inteiros.
statusé um campoIntegerFieldque utiliza ochoicesStatusCompra.choicese tem o valor padrãoStatusCompra.CARRINHO, que no caso é1.
Opcionalmente, poderíamos ter criado uma entidade
StatusComprae utilizado um campoForeignKeypara ela. No entanto, como temos um número pequeno de status, optamos por utilizar oIntegerFieldcomchoices.
core/models/__init__.py:from .compra import Compra
Adicionando a model Compra ao Admin
Compra ao admin.py do app core:...
from core.models import Compra
...
@admin.register(Compra)
class CompraAdmin(admin.ModelAdmin):
list_display = ('usuario', 'status')
ordering = ('usuario', 'status')
list_per_page = 10
Executando as migrações
O seu projeto deve ficar assim:

Testando a model Compra
Compra no Admin do Django.Finalizando
feat: criação da entidade Compra integrada ao usuário do projeto
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.
ItensCompra ao arquivo core/models/compra.py:...
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, utilizamosmodels.CASCADE, pois queremos que, ao deletar uma compra, todos os itens da compra sejam deletados também.
No atributo
livro, utilizamosmodels.PROTECT, pois queremos impedir que um livro seja deletado se ele estiver associado a um item de compra.
Ainda no
livro, utilizamosrelated_name='+', pois não queremos que oItensCompratenha um atributolivro.
__init__.py dos models:from .compra import Compra, ItensCompra
O seu projeto deve ficar assim:

core_itenscompra foi criada no banco de dados.ItensCompra no Admin do Django.feat: criação dos 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.
admin.py do app core, modifique o código das models Compra e ItensCompra da seguinte forma: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 = 10
inlines = [ItensCompraInline]
Desta forma, quando você editar uma compra no
Admindo Django, você verá os itens da compra logo abaixo do formulário de edição da compra.
Opcionalmente, você pode utilizar o
StackedInlineao invés doTabularInline. Experimente e veja a diferença.
Admin do Django.feat: uso de TabularInline e StackedInline no Admin para Itens da Compra
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
compra.py dentro da pasta serializers do app core:touch core/serializers/compra.py
compra.py recém criado:from rest_framework.serializers import ModelSerializer
from core.models import Compra
class CompraSerializer(ModelSerializer):
class Meta:
model = Compra
fields = '__all__'
CompraSerializer no arquivo __init__.py dos serializers:from .compra import CompraSerializer
Criação da Viewset de Compra
compra.py dentro da pasta views do app core:touch core/views/compra.py
compra.py recém criado: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
CompraViewSet no arquivo __init__.py das views:from .compra import CompraViewSet
URL para listagem de compras
urls.py do app core:...
from core.views import (
AutorViewSet,
CategoriaViewSet,
CompraViewSet, # inclua essa linha
EditoraViewSet,
LivroViewSet,
UserViewSet,
)
...
router.register(r'compras', CompraViewSet)
...
feat: criação do endpoint para a listagem básica de compras
Inclusão do e-mail 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.
Compra, inclua o seguinte código:...
from rest_framework.serializers import CharField, ModelSerializer
...
class CompraSerializer(ModelSerializer):
usuario = CharField(source='usuario.email', read_only=True) # inclua essa linha
...
O parâmetro
sourceindica qual campo do modelCompraserá utilizado para preencher o campousuariodo serializer.
O parâmetro
read_onlyindica que o campousuarionão será utilizado para atualizar o modelCompra.
feat: inclusão do e-mail do usuário na listagem da compra
Inclusão do status da compra na listagem da compra
De forma semelhante ao e-mail do usuário, vamos incluir o status da compra na listagem da compra.
Compra, inclua o seguinte código:...
class CompraSerializer(ModelSerializer):
status = CharField(source='get_status_display', read_only=True) # inclua essa linha
...
O parâmetro
sourceindica qual método do modelCompraserá utilizado para preencher o campostatusdo serializer. Sempre que utilizamos um campo do tipoIntegerChoices, podemos utilizar o métodoget_<nome_do_campo>_displaypara obter a descrição do campo.
O parâmetro
read_onlyindica que o campostatusnão será utilizado para atualizar o modelCompra.
feat: inclusão do status da compra na listagem da 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.
De forma semelhante ao que fizemos no Admin, vamos incluir os itens da compra na listagem de compras.
ItensCompra, no arquivo serializers/compra.py:...
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=Trueindica que o campoitensé uma lista de itens.
O parâmetro
read_only=Trueindica que o campoitensnão será utilizado para atualizar o modelCompra.
feat: visualização dos itens da compra na listagem da compra
Mostrando os detalhes dos itens da compra na listagem de compras
ItensCompra, modifique o código:class ItensCompraSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = '__all__'
depth = 1
O parâmetro
depth=1indica que o serializer deve mostrar os detalhes do modelItensCompra. O valor1indica que o serializer deve mostrar os detalhes do modelItensComprae dos models relacionados a ele (nesse caso, olivro). Se o valor fosse2, o serializer mostraria os detalhes do modelItensCompra, dos models relacionados a ele e dos models relacionados aos models relacionados a ele (nesse caso, acategoria, aeditorae oautor).
depth e veja o resultado no navegador.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.
ItensCompraSerializer, modifique a linha fields:fields = ('livro', 'quantidade')
O parâmetro
fieldsindica quais campos do modelItensCompraserã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.
feat: limitando os campos dos itens da compra 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 este campo na listagem de compras.
SerializerMethodField no arquivo serializers/compra.py:from rest_framework.serializers import CharField, ModelSerializer, SerializerMethodField
ItensCompraSerializer, para que fique desse jeito: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
SerializerMethodFieldindica que o campototalnão existe no modelItensCompra. Ele será calculado pelo métodoget_total.
O método
get_totalrecebe como parâmetro o objetoinstance, que representa o item da compra. A partir dele, podemos acessar os campos do item da compra, comoquantidadeelivro.preco.
O método
get_totalretorna o valor do campototal, 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
totalno atributofieldsdo serializer.
feat: mostrando o total do item 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 este campo na listagem de compras.
model Compra, inclua o seguinte código:...
@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
propertyindica que o campototalnão existe no modelCompra. Ele será calculado pelo métodototal.
O método
totalretorna o valor do campototal, que é calculado pela soma dos totais dos itens da compra, que é calculado pelo preço do livro multiplicado pela quantidade do item da compra.
total no serializer de Compra. No CompraSerializer, inclua o seguinte código:...
fields = ('id', 'usuario', 'status', 'total', 'itens')
...
O parâmetro
fieldsindica quais campos do modelCompraserã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.
feat: inclusão do total da compra na listagem de compras
Inclusão do total da compra no Admin
Para finalizar, vamos incluir o total da compra no Admin do Django.
admin.py do app core, modifique o código da model Compra:@admin.register(Compra)
class CompraAdmin(admin.ModelAdmin):
list_display = ('usuario', 'status', 'total_formatado') # mostra na listagem
ordering = ('usuario', 'status')
list_per_page = 10
inlines = [ItensCompraInline]
readonly_fields = ("total_formatado",) # mostra dentro do formulário
@admin.display(description="Total")
def total_formatado(self, obj):
"""Exibe R$ 123,45 em vez de 123.45."""
return f"R$ {obj.total:.2f}"
O método
total_formatadoé um método especial doadminque é chamado para formatar o valor do campototal. Ele recebe como parâmetro o objetoobj, que representa a compra. A partir dele, podemos acessar os campos da compra, comototal.
O método
total_formatadoretorna o valor do campototalformatado como uma string, com duas casas decimais e o símbolo de real (R$).
O parâmetro
readonly_fieldsindica que o campototal_formatadoé apenas para leitura. Isso significa que ele não será editável no formulário de edição da compra.
O parâmetro
@admin.display(description="Total")indica que o campototal_formatadoserá exibido com o título “Total” na listagem doAdmin.
O parâmetro
list_displayindica quais campos serão exibidos na listagem doAdmin. O campototal_formatadoserá exibido na listagem, com o título “Total”.
Admin do Django e verifique se o total da compra está sendo exibido corretamente.feat: inclusão do total da compra no Admin
Vamos primeiro definir o que é necessário para criar uma nova compra. Para isso, precisamos informar o usuário e os itens da compra. Os itens da compra são compostos pelo livro e pela quantidade.
O formato dos dados para criar uma nova compra é o seguinte:
{
"usuario": 1,
"itens": [
{
"livro": 1,
"quantidade": 1
},
{
"livro": 2,
"quantidade": 2
}
]
}
Criando serializers para criação de compras
Como estamos lidando com dados aninhados (compra com vários itens), precisamos criar serializers específicos para entrada de dados.
1. ItensCompraCreateUpdateSerializer
Esse serializer será usado para criar os itens de uma compra. Ele é simples, pois requer apenas o livro e a quantidade.
No início do arquivo serializers/compra.py, adicione:
class ItensCompraCreateUpdateSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = ('livro', 'quantidade')
2. CompraCreateUpdateSerializer
Agora vamos criar o serializer da Compra, utilizando o serializer acima no campo itens.
Ainda no serializers/compra.py, inclua o seguinte código:
class CompraCreateUpdateSerializer(ModelSerializer):
itens = ItensCompraCreateUpdateSerializer(many=True)
class Meta:
model = Compra
fields = ('usuario', 'itens')
O parâmetro
many=Trueindica que o campoitensé uma lista de itens de compra.
__init__.py dos serializers:from .compra import (
CompraCreateUpdateSerializer,
CompraSerializer,
ItensCompraCreateUpdateSerializer,
ItensCompraSerializer,
)
Atualizando a view para usar o serializer de criação
Vamos alterar o viewset de Compra para usar o novo serializer, nas operações de criação e alteração.
views/compra.py altere o viewset de Compra para usar o novo serializer:...
from core.serializers import CompraCreateUpdateSerializer, CompraSerializer
...
class CompraViewSet(ModelViewSet):
queryset = Compra.objects.all()
serializer_class = CompraSerializer
def get_serializer_class(self):
if self.action in ('create', 'update', 'partial_update'):
return CompraCreateUpdateSerializer
return CompraSerializer
Testando a criação de compra
POST no endpoint /compras/, por exemplo no ThunderClient:{
"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.
Entendendo o erro
Esse erro acontece porque o DRF, por padrão, não sabe como salvar campos aninhados (como os itens da compra). Precisamos sobrescrever o método create no serializer da Compra.
Implementando o método create
Atualize o CompraCreateUpdateSerializer no serializers/compra.py para incluir o método:
...
class CompraCreateUpdateSerializer(ModelSerializer):
itens = ItensCompraCreateUpdateSerializer(many=True)
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
Explicação
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
createrecebe um parâmetrovalidated_data, que são os dados validados que estão sendo criados.
validade_data.pop('itens')remove os itens da compra dos dados validados. Isso é necessário, pois os itens da compra são criados separadamente.
O comando
Compra.objects.create(**validated_data)cria a compra com os dados validados, exceto os itens da compra.
O comando
ItensCompra.objects.create(compra=compra, **item_data)cria novos itens com os dados validados. Ele liga os itens da compra à compra recém criada, através do parâmetrocompra=compra.
Conclusão
feat: criação de um endpoint para criar novas compras
Entendendo o problema
compras/1/ (ou aquela que você preferir) no ThunderClient, utilizando o método PUT:{
"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.
Esse erro acontece porque os itens da compra vêm de uma tabela relacionada (
ItensCompra) e o DRF, por padrão, não sabe como atualizar campos aninhados. Precisamos, portanto, sobrescrever o método update() do serializer.
Sobrescrevendo o método update
serializers/compra.py, altere o CompraCreateUpdateSerializer adicionando o seguinte: 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)
Explicando o método update
validated_data.pop('itens', []): remove os dados dos itens para tratar separadamente;compra.itens.all().delete(): remove todos os itens antigos da compra;ItensCompra.objects.create(...): recria cada item com os novos dados;super().update(...): atualiza os demais campos da compra.Testando o endpoint no ThunderClient
PUT, para atualizar a compra de forma completa;PATCH, para atualizar a compra de forma parcial.
Finalize com um commit
feat: criação de um endpoint para atualizar 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.
serializers/compra.py, crie um novo serializador chamado CompraListSerializer:...
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 (
CompraCreateUpdateSerializer,
CompraListSerializer, # novo
CompraSerializer,
ItensCompraCreateUpdateSerializer,
ItensCompraListSerializer, # novo
ItensCompraSerializer,
)
...
viewset de Compra, vamos alterar o serializer_class para usar o novo serializer:...
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', 'partial_update'):
return CompraCreateUpdateSerializer
return CompraSerializer
...
feat: criação de um serializador específico para a listagem de compras
Nesta aula, vamos aprimorar a criação de uma compra na nossa API. Em vez de enviar o campo usuario no corpo da requisição, vamos configurar o serializer para usar automaticamente o usuário que está autenticado no sistema. Isso torna a API mais segura e prática para o consumidor.
Ajustes no serializer
Abra o arquivo serializers/compra.py e adicione as seguintes importações:
from rest_framework.serializers import (
CharField,
CurrentUserDefault, # novo
HiddenField, # novo
ModelSerializer,
SerializerMethodField,
)
Agora, no CompraCreateUpdateSerializer, substitua o campo usuario para que ele seja preenchido automaticamente com o usuário autenticado:
class CompraCreateUpdateSerializer(ModelSerializer):
usuario = HiddenField(default=CurrentUserDefault())
class Meta:
model = Compra
fields = ('id', 'usuario', 'itens')
O campo
usuarioagora é umHiddenField, ou seja, não aparece nem na requisição nem na resposta.
Com
CurrentUserDefault(), o DRF preenche automaticamente com o usuário logado no momento da requisição.
Teste no Thunder Client
Faça um teste enviando uma requisição POST para o endpoint /compras/, com o seguinte corpo:
{
"itens": [
{
"livro": 2,
"quantidade": 2
}
]
}
Observe que não precisamos mais informar o usuário, pois ele será automaticamente associado à compra com base no token de autenticação.
Esse comportamento só funciona corretamente se a requisição estiver autenticada (via token ou sessão).
Commit
feat: criação de uma compra a partir do usuário autenticado
Atualmente, qualquer usuário pode visualizar todas as compras cadastradas na API, o que não é o comportamento desejado. Vamos ajustar isso para que:
Atualizando o ViewSet
Abra o arquivo views/compra.py e localize o CompraViewSet. Vamos sobrescrever o método get_queryset:
from rest_framework.viewsets import ModelViewSet
from core.models import Compra
from core.serializers.compra import (
CompraCreateUpdateSerializer,
CompraListSerializer,
CompraSerializer,
)
class CompraViewSet(ModelViewSet):
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)
...
Explicação do código
get_queryset é chamado sempre que o DRF precisa buscar objetos no banco de dados.self.request.user para acessar o usuário autenticado.administradores, ele verá todas as compras.Com isso, garantimos uma separação adequada de permissões entre usuários comuns e administradores.
Testando a funcionalidade
/compras/ e confirme que apenas as compras feitas por esse usuário estão visíveis.administradores).Commit
feat: filtrando apenas as compras do usuário autenticado
Objetivo da aula
Revisão rápida
Fluxo de Validação no DRF
data=request.data).is_valid() → começa a validação.
validators=[]).validate_<campo> (ex.: validate_quantidade).validate(self, attrs) para regras entre campos.serializer.save() grava no banco.Não permitindo itens com quantidade zero
Nesse momento, é possível criar uma compra com um item com quantidade zero. Vamos validar isso.
serializers/compra.py, vamos alterar o serializer ItensCompraCreateUpdateSerializer para validar a quantidade do item da compra:...
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.
serializers/compra.py, vamos alterar o serializer ItensCompraCreateUpdateSerializer para validar a quantidade de itens em estoque, de forma a não permitir que a quantidade de itens solicitada seja maior do que a quantidade em estoque:...
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
validatepermite 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).
feat: validando a quantidade de itens na compra
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 e-mail da Editora em minúsculas.
serializers/editora.py, vamos alterar o serializer EditoraSerializer para formatar o e-mail 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 e-mail da Editora em minúsculas.
feat: validando e formatando dados antes de salvar
Até agora, o preço do item da compra era calculado dinamicamente a partir do livro associado. Isso gera um problema: se o preço do livro mudar, o histórico das compras anteriores também mudaria, o que não é desejado.
Objetivo desta aula: manter registrado no banco o preço do livro no momento da compra, garantindo que o histórico seja preservado.
Incluindo o campo preco em ItensCompra
models/compra.py, adicione o campo preco:...
class ItensCompra(models.Model):
...
preco = models.DecimalField(max_digits=7, decimal_places=2, default=0)
Gravando o preço do livro na criação da compra
serializers/compra.py, altere o método create do CompraCreateUpdateSerializer para registrar o preço do livro:...
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 # preço do livro no momento da compra
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.
Calculando o total do item de compra baseado no preço do livro
ItensCompraSerializer, atualize a função get_total para usar o preço gravado no item, e não mais o preço atual do livro: def get_total(self, instance):
return instance.quantidade * instance.preco
Calculando o total da compra com base no preço do item
models/compra.py, altere a propriedade total da model Compra:...
@property
def total(self):
return sum(item.preco * item.quantidade for item in self.itens.all())
...
Agora o total da compra considera o preço registrado no item, e não o preço atual do livro.
Inclua o preco nos campos fields dos serializers
class ItensCompraCreateUpdateSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = ('livro', 'quantidade', 'preco') # mudou
class ItensCompraListSerializer(ModelSerializer):
livro = CharField(source='livro.titulo', read_only=True)
class Meta:
model = ItensCompra
fields = ('quantidade', 'preco', 'livro') # mudou
depth = 1
class ItensCompraSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = ('livro', 'quantidade', 'preco', 'total') # mudou
...
Testando
Gravando o preço do livro na atualização do item da compra
No mesmo serializer (CompraCreateUpdateSerializer), ajuste o método update:
...
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 # grava o preço histórico
ItensCompra.objects.create(compra=compra, **item)
compra.save()
return super().update(compra, validated_data)
...
Testando
/compras/).ItensCompra.Commit
feat: Gravação do preço do livro no item da compra
Atualmente, não existe nenhum registro da data da compra. Vamos incluir esse campo para que a data seja definida automaticamente no momento da criação da compra.
No arquivo models/compra.py, adicione o campo data na entidade 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 é do tipo
DateTimeField, que armazena tanto a data quanto a hora da compra.
O parâmetro
auto_now_add=Truefaz com que o campo seja preenchido automaticamente com a data e hora atuais no momento em que a compra é criada.
Migração
Agora, execute as migrações.
Durante a criação da migration, será exibido um erro informando que o campo data não pode ser nulo.
Escolha a opção 1, que preenche automaticamente o campo com a data atual (timezone.now).
Depois, aplique as migrações também no banco publicado, caso você esteja utilizando.
Modificando o serializer de compra para mostrar a data da compra
Para que a data apareça no endpoint, vamos incluir esse campo no serializer de Compra.
No arquivo serializers/compra.py, modifique o código da seguinte forma:
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
...
Testando
Incluindo a data no Admin do Django
No arquivo admin.py do app core, modifique o código da model Compra:
@admin.register(Compra)
class CompraAdmin(admin.ModelAdmin):
@admin.display(description="Total")
def total_formatado(self, obj):
"""Exibe R$ 123,45 em vez de 123.45."""
return f"R$ {obj.total:.2f}"
list_display = ('usuario', 'status', 'total_formatado', 'data') # mostra na listagem
ordering = ('usuario', 'status', 'data') # ordena por esses campos
search_fields = ('usuario__email', 'status') # campos pesquisáveis
list_filter = ('status', 'data') # filtros laterais
list_per_page = 10
inlines = [ItensCompraInline]
readonly_fields = ('data', 'total_formatado',) # campos somente leitura
...
Exercício
data_atualizacao, que armazena a data da última atualização da compra.
auto_now=True.Compra.list_display e readonly_fields do Admin.data para data_criacao.Commit
feat: registrando a data da compra
Contexto
Em qualquer sistema de e-commerce ou livraria online, é essencial registrar como cada compra foi paga. Além de organizar a operação (financeiro, emissão de notas, devoluções), também permite gerar estatísticas úteis:
Implementação no Model
No arquivo models/compra.py, adicione o campo tipo_pagamento:
...
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 que está acontecendo aqui?
IntegerChoices cria uma lista de opções amigáveis para o campo.Execute as migrações.
Exibição no Serializer
No arquivo serializers/compra.py, inclua o novo campo:
...
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 tipoCharField, que mostra o tipo de pagamento da compra. O parâmetrosourceindica 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 campotipo_pagamento.
O campo
tipo_pagamentofoi incluído no atributofieldsdo serializer.
Testando
compra = Compra.objects.first()
print(compra.tipo_pagamento) # mostra o valor interno (ex: 1)
print(compra.get_tipo_pagamento_display()) # mostra o valor legível (ex: 'Cartão de Crédito')
Atividade Prática
Commit
feat: adicionando tipo de pagamento à entidade de Compra
Objetivo
Entender o conceito de ações personalizadas (actions) no Django REST Framework e aprender a criar uma na prática.
O que são ações personalizadas?
No DRF, os ViewSets já oferecem automaticamente as ações padrão:
list: listar objetosretrieve: buscar objeto específicocreate: criar novo objetoupdate / partial_update: atualizar objetodestroy: excluir objetoEssas ações cobrem o básico do CRUD.
Mas muitas vezes precisamos de funcionalidades extras, que não se encaixam nesses métodos.
É aí que entram as ações personalizadas: endpoints adicionais que podemos criar em um ViewSet, usando o decorador @action.
Exemplos práticos:
Alterando o preço de um livro
Nosso primeiro exemplo será uma ação para alterar o preço de um livro específico, passando o novo preço no corpo da requisição e o ID do livro na URL.
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=7, decimal_places=2)
def validate_preco(self, preco):
'''Valida se o preço é um valor positivo.'''
if preco <= 0:
raise ValidationError('O preço deve ser um valor positivo.')
return preco
...
__init__.py dos serializers:...
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.
views/livro.py, vamos criar um método alterar_preco na view LivroViewSet:from rest_framework import status
from rest_framework.decorators import action
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
@actioncria um endpoint para a açãoalterar_preco, no formatoapi/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 nopkfornecido.
O método
LivroAlterarPrecoSerializeré um serializer específico para a açãoalterar_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
Responseretorna uma resposta HTTP.
O status
HTTP_200_OKindica que a requisição foi bem sucedida.
Testando a action
{
"preco": 59.90
}
Documentando a action no Swagger
views/livro.py, adicione a documentação para o Swagger:from drf_spectacular.utils import extend_schema
...
@extend_schema(
request=LivroAlterarPrecoSerializer,
responses={200: None},
description="Altera o preço de um livro específico.",
summary="Alterar preço do livro",
)
@action(detail=True, methods=['patch'])
def alterar_preco(self, request, pk=None):
...
O decorador
@extend_schemaé usado para documentar a action no Swagger.
Teste novamente no Swagger e veja que a documentação foi atualizada. Commit
Faça o commit com a mensagem:
feat: alterando o preço de um livro
Objetivo
Aprender a criar ações personalizadas que atuam sobre o conjunto inteiro de objetos, e não apenas em um item específico.
Quando usar detail=False?
detail=True cria endpoints para um item específico, como:
/api/livros/{id}/alterar_preco/
detail=False cria endpoints para o conjunto de registros, como:
/api/livros/mais_vendidos/
/api/compras/relatorio_vendas_mes/
Essas ações são ideais para consultas, estatísticas e relatórios.
Exemplo: Relatório de vendas do mês
No arquivo views/compra.py, dentro da CompraViewSet, crie o relatório:
from django.utils import timezone
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
class CompraViewSet(ModelViewSet):
...
@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,
)
Explicação
@action(detail=False, methods=['get']): cria o endpoint /api/compras/relatorio_vendas_mes/.timezone.now(): captura a data e hora atuais.inicio_mes: marca o primeiro dia do mês(para filtrar compras do mês atual).Compra.objects.filter(...): filtra compras finalizadas no mês atual.sum(compra.total for compra in compras): soma os valores totais.Testando
No Swagger, acesse:
GET /compras/relatorio_vendas_mes/
A resposta será algo como:
{
"status": "Relatório de vendas deste mês",
"total_vendas": 1249.80,
"quantidade_vendas": 8
}
Documentando a action no Swagger
views/compra.py, adicione a documentação para o Swagger:from drf_spectacular.utils import extend_schema
...
@extend_schema(
request=None,
responses={200: None},
description="Gera um relatório de vendas do mês atual.",
summary="Relatório de vendas do mês",
)
@action(detail=False, methods=['get'])
def relatorio_vendas_mes(self, request):
...
Commit
feat: adicionando relatório de vendas mensal em compras
Objetivo
Aprender a criar uma ação personalizada que realiza ajustes em vários registros (compra e itens de estoque), garantindo integridade transacional e validação efetiva durante o processo de finalização de compra.
Contexto do problema
Quando o usuário faz uma compra, ela inicia no status CARRINHO e ainda não impacta o estoque dos livros. Ao finalizar a compra, o status passa para FINALIZADO e o sistema precisa:
Implementação do endpoint de finalização
No arquivo views/compra.py, crie a ação personalizada finalizar dentro do CompraViewSet:
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):
...
@extend_schema(
request=None,
responses={200: None, 400: None},
description="Finaliza a compra, atualizando o estoque dos livros.",
summary="Finalizar compra",
)
@action(detail=True, methods=["post"])
def finalizar(self, request, pk=None):
compra = self.get_object()
# Verifica se a compra já foi finalizada
if compra.status == Compra.StatusCompra.FINALIZADO:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={'status': 'Compra já finalizada'}
)
# Garante integridade transacional durante a finalização
with transaction.atomic():
for item in compra.itens.all():
# Valida se o estoque é suficiente para cada livro
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,
}
)
# Atualiza o estoque dos livros
item.livro.quantidade -= item.quantidade
item.livro.save()
# Finaliza a compra: atualiza status
compra.status = Compra.StatusCompra.FINALIZADO
compra.save()
return Response(status=status.HTTP_200_OK, data={'status': 'Compra finalizada'})
@action gera o endpoint api/compras/{id}/finalizar para esse recurso.with transaction.atomic() garante que toda operação será executada com consistência: se algo falhar, nada será salvo.Commit
feat: finalizando a compra e atualizando a quantidade de itens em estoque
Vamos criar uma ação personalizada na API para listar os livros que venderam mais de 10 unidades. Essa funcionalidade será implementada como um endpoint de coleção, aplicável a todos os livros cadastrados.
Ajustando a Model
Primeiro, inclua o parâmetro related_name no campo livro da entidade ItensCompra em models/compra.py. Isso facilitará consultas reversas e deixará o código mais expressivo.
class ItensCompra(models.Model):
...
livro = models.ForeignKey(Livro, on_delete=models.PROTECT, related_name='itens_compra')
...
Após o ajuste, execute as migrações:
pdm migrate
Criando o Serializer
Para garantir padronização e flexibilidade de retorno, utilize um serializer específico na resposta:
class LivroMaisVendidoSerializer(ModelSerializer):
total_vendidos = IntegerField()
class Meta:
model = Livro
fields = ['id', 'titulo', 'total_vendidos']
Implementando a Ação Personalizada
No arquivo views/livro.py, inclua o método mais_vendidos na view LivroViewSet:
from django.db.models import Q, Sum
...
from core.serializers import LivroMaisVendidoSerializer
class LivroViewSet(viewsets.ModelViewSet):
queryset = Livro.objects.all()
serializer_class = LivroSerializer # Seu serializer padrão
@action(detail=False, methods=['get'])
def mais_vendidos(self, request):
livros = Livro.objects.annotate(
total_vendidos=Sum(
'itens_compra__quantidade',
filter=Q(itens_compra__compra__status=Compra.StatusCompra.FINALIZADO)
)
).filter(total_vendidos__gt=10).order_by('-total_vendidos')
serializer = LivroMaisVendidoSerializer(livros, many=True)
if not serializer.data:
return Response(
{"detail": "Nenhum livro excedeu 10 vendas."},
status=status.HTTP_200_OK
)
return Response(serializer.data, status=status.HTTP_200_OK)
O decorador
@action(detail=False)define um endpoint de coleção no formato/api/livros/mais_vendidos/.
O método
annotatesoma o total vendido para cada livro por meio do relacionamento reverso (itens_compra__quantidade).
O
filterdentro doSumassegura que apenas itens de compras finalizadas sejam considerados.
O
filter(total_vendidos__gt=10)retorna apenas livros com mais de 10 unidades vendidas.
O método Q permite aplicar filtros complexos, garantindo que apenas itens de compras finalizadas sejam considerados.
Os resultados são filtrados para retornar apenas livros que tenham mais de 10 unidades vendidas e já vêm ordenados do maior para o menor total.
O serializer facilita a manutenção e a extensão da resposta.
Documentação Swagger/OpenAPI com drf-spectacular
Se estiver utilizando drf-spectacular, acrescente a documentação da action com o decorador @extend_schema:
class LivroViewSet(ModelViewSet):
...
@extend_schema(
summary="Lista os livros mais vendidos",
description="Retorna os livros que venderam mais de 10 unidades.",
responses={
200: LivroMaisVendidoSerializer(many=True)
},
)
@action(detail=False, methods=['get'])
def mais_vendidos(self, request):
...
Assim, o endpoint /api/livros/mais_vendidos/ será exibido automaticamente na documentação Swagger com os campos id, título e total_vendidos, e poderá ser testado por qualquer consumidor da API.
Exemplo de resposta
Ao realizar uma requisição GET para /api/livros/mais_vendidos/, o retorno será deste formato:
[
{
"id": 1,
"titulo": "O Código Limpo",
"total_vendidos": 33
},
{
"id": 2,
"titulo": "O Codificador Limpo",
"total_vendidos": 25
}
]
Se nenhum livro exceder 10 vendas, o resultado será:
{
"detail": "Nenhum livro excedeu 10 vendas."
}
Commit
Faça o commit com a mensagem:
feat: listando livros com mais de 10 cópias vendidas
Objetivo: criar uma action personalizada que permita ajustar (aumentar ou diminuir) o estoque de um livro de forma segura, impedindo que o valor fique negativo.
Serializer específico
Adicione em serializers/livro.py:
class LivroAjustarEstoqueSerializer(Serializer):
quantidade = serializers.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
Atualize serializers/__init__.py:
from .livro import (
LivroAjustarEstoqueSerializer,
...
)
Action na ViewSet
Em views/livro.py, adicione a action ao LivroViewSet:
...
from drf_spectacular.utils import OpenApiResponse, extend_schema
from .serializers import LivroAjustarEstoqueSerializer
class LivroViewSet(ModelViewSet):
...
@extend_schema(
summary="Ajusta o estoque de um livro",
description="Aumenta ou diminui o estoque; impede resultado negativo.",
request=LivroAjustarEstoqueSerializer,
responses={
200: OpenApiResponse(
response=None,
description="Estoque ajustado com sucesso.",
examples=[
{
"status": "Quantidade ajustada com sucesso",
"novo_estoque": 30
}
]
),
400: OpenApiResponse(
description="Erro de validação",
examples=[
{"quantidade": "A quantidade em estoque não pode ser negativa."}
]
),
},
)
@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
)
Testando o Endpoint
Para ajustar o estoque, envie uma requisição POST para /api/livros/{id}/ajustar_estoque/ com um JSON contendo o campo quantidade.
Exemplo de entrada:
{"quantidade": 5}
Exemplo de resposta bem-sucedida:
{
"status": "Quantidade ajustada com sucesso",
"novo_estoque": 30
}
Exemplo de resposta de erro:
{
"quantidade": [
"A quantidade em estoque não pode ser negativa."
]
}
Teste e Validação
Commit
Faça o commit com a mensagem:
feat: ajustando o estoque de um livro
Até agora, nossa API lista todos os livros, sem possibilidade de filtragem. Nesta aula, vamos implementar filtros para facilitar consultas específicas, como por categoria, editora e autores.
Preparando o Filter Backend no ViewSet
O pacote django-filter já está instalado no projeto, o que permite criar filtros dinâmicos e declarativos.
No arquivo views/livro.py, vamos configurar o LivroViewSet para usar filtros:
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
from .models import Livro
from .serializers import LivroSerializer
class LivroViewSet(viewsets.ModelViewSet):
queryset = Livro.objects.all()
serializer_class = LivroSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['categoria__descricao', 'editora__nome'] # Campos para filtragem
filter_backendsdefine o backend que aplica os filtros na query.
filterset_fieldsindica quais campos do model (ou relacionamentos) estarão disponíveis para filtro.
Usamos a notação de dupla underscore (
__) para acessar campos de modelos relacionados.
Testando a Filtragem
Com essa configuração, o endpoint GET /api/livros/ aceita parâmetros de consulta, como:
GET /api/livros/?categoria__descricao=PythonGET /api/livros/?editora__nome=NovatecGET /api/livros/?categoria__descricao=Python&editora__nome=NovatecNo Swagger, acessando o endpoint livros/, clique em Try it out e verá campos para filtrar por categoria__descricao e editora__nome.
Também pode testar via chamadas HTTP diretas com ferramentas como ThunderClient ou curl.
Acrescentando Filtros em Outros Models
De modo semelhante, acrescente filtros nos viewsets dos models Autor, Categoria, Editora e Compra.
Commit
Faça o commit com a mensagem:
feat: adicionando filtros para listagem de recursos
A busca textual permite pesquisar dentro dos valores de texto dos campos de um banco de dados, facilitando encontrar registros que contenham determinado texto. Essa funcionalidade é aplicável a campos como CharField e TextField.
Configurando a Busca Textual no LivroViewSet
No arquivo views/livro.py, vamos alterar o LivroViewSet para incluir o backend SearchFilter e definir quais campos permitirão a busca textual:
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
...
class LivroViewSet(viewsets.ModelViewSet):
...
filter_backends = [DjangoFilterBackend, SearchFilter] # Adicionando SearchFilter
filterset_fields = ['categoria__descricao', 'editora__nome'] # Campos para filtro
search_fields = ['titulo'] # Campos para busca textual
filter_backendsinclui agora tantoDjangoFilterBackendpara filtros específicos por campos, quantoSearchFilterpara busca textual.
search_fieldsdefine os campos que terão busca textual ativada, neste caso, o campotitulodo livro.
Utilizando a Busca Textual
Com esta configuração, para buscar livros que contenham uma palavra no título, basta fazer uma requisição GET para o endpoint com o parâmetro search.
Exemplo para buscar livros com a palavra “python” no título:
GET /api/livros/?search=python
No Swagger, o campo search aparecerá automaticamente para preenchimento ao testar o endpoint de listagem de livros.
Combine filtros específicos (com filterset_fields) e busca textual para refinar resultados.
Commit
Faça commit com a mensagem padrão para recursos novos:
feat: adicionando busca textual
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.
ViewSet:filter_backends, adicionando o Backend OrderingFilter que irá processar a ordenação; eordering_fields, contendo os campos que permitirão a ordenação.ordering com o campo que será utilizado como padrão para ordenação.LivroViewSet em views/livro.py ficará assim:...
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']
...
ordering na URL, com o valor do campo a ser ordenado.ordering, a ordenação será feita pelo campo definido no atributo ordering, nesse caso, titulo:
python no título, a URL ficaria assim:
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.
views/compra.py, vamos alterar o atributo filterset_fields, na viewset de Compra para filtrar as compras por data.ordering_fields, na viewset de Compra para ordenar as compras por data....
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
Autor, Categoria, Editora e Compra.feat: adicionando ordenação
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.
serializers/compra.py, vamos alterar o serializer chamado CompraCreateUpdateSerializer: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_createretorna um objetoCompraexistente ou cria um novo objetoComprase ele não existir.
O método
filterretorna um objetoItensCompraque atenda aos critérios de pesquisa.
O método
firstretorna o primeiro objetoItensCompraque atenda aos critérios de pesquisa ouNonese não houver objetos.
40. Inclusão do total da compra no modelo
Nesta etapa, vamos adicionar um campo total ao modelo Compra, responsável por armazenar o valor total de cada compra.
Isso traz ganhos de performance e facilidade nas consultas, permitindo ordenar e filtrar diretamente pelo total no banco de dados.
1. Adicionando o campo total
No arquivo models/compra.py, inclua o campo abaixo:
class Compra(models.Model):
...
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
...
2. Calculando o total automaticamente
Vamos sobrescrever o método save para calcular o total antes de salvar a compra:
class Compra(models.Model):
...
def save(self, *args, **kwargs):
if self.pk:
self.total = sum(item.preco * item.quantidade for item in self.itens.all())
super().save(*args, **kwargs)
Explicando o método:
saveé chamado sempre que um objeto é salvo no banco.
super().save(*args, **kwargs)chama o método original da classe pai.
self.itens.all()retorna todos os itens associados à compra.
sum(...)soma o total de cada item (preço × quantidade).
O
if self.pkgarante que o cálculo só ocorra depois da criação inicial da compra.
3. Removendo a property antiga
Como o total agora está armazenado no banco, podemos remover o cálculo dinâmico da property total:
class Compra(models.Model):
...
# Remover o trecho abaixo, se existir:
# @property
# def total(self):
# return sum(item.preco * item.quantidade for item in self.itens.all())
...
4. Salvando o total no serializer
No serializer CompraCreateUpdateSerializer, garanta que o método create salve a compra após a criação dos itens, para atualizar o campo total:
class CompraCreateUpdateSerializer(ModelSerializer):
...
def create(self, validated_data):
...
compra.save() # Linha adicionada para atualizar o campo total
return compra
...
5. Executando migrações
Após essas alterações, execute as migrações para atualizar o banco de dados:
pdm migrate
6. Atualizando compras existentes
Para atualizar o campo total das compras já existentes, utilize o shell do Django:
for compra in Compra.objects.all():
compra.save()
Isso recalcula e salva o total de todas as compras já registradas.
7. Testando o funcionamento
8. Ordenações e consultas
Com o campo total armazenado, podemos realizar consultas otimizadas:
Ordenar pelo total (decrescente):
compras = Compra.objects.all().order_by('-total')
Filtrar compras com valor mínimo:
compras = Compra.objects.filter(total__gte=100)
9. Commit
Por fim, registre a alteração no controle de versão:
feat: adicionando o total da compra
Resumo da aula
Adicionamos o campo total ao modelo Compra.
O valor total é calculado automaticamente no save().
A property antiga foi removida.
O serializer foi atualizado para garantir o salvamento.
Atualizamos os registros existentes e testamos consultas com base no total.
O projeto Garagem é um projeto de uma garagem de carros. O objetivo é praticar aquilo que foi visto nesse tutorial, no projeto da Livraria.
Seguindo aquilo que você já aprendeu na criação do projeto da Livraria, crie um novo projeto, a partir do template.
garagem.Nomeie o commit como sendo:
feat: Criação do projeto.
Acessorio:
descricao (string, máximo 100 caracteres).__str__ (retorna o id e a a descrição).Ar condicionado, Direção hidráulica, Vidros elétricos, Travas elétricas, Alarme, Airbag, Freios ABS.Cor:
nome (string, máximo 40 caracteres).__str__ (retorna o nome e o id).Preto, Branco, Prata, Vermelho, Cinza, Grafite.Modelo:
nome (string, máximo 80 caracteres).marca(string, máximo 80 caracteres, não obrigatório).categoria (string, máximo 80 caracteres, não obrigatório).__str__ (retorna id, marca (maiúsculas) e nome do modelo (maiúsculas).KA, FIESTA, ECOSPORT, RANGER, ONIX, PRISMA, TRACKER, S10, GOL, POLO, TAOS, AMAROK, ARGO, TORO, UNO, CRONOS, COMPASS, CIVIC, HR-V, FIT, CITY, HB20, CRETA, TUCSON, KICKS, FRONTIER, 208, 3008, C3, C4.Veiculo no projeto Garagem.
Veiculo, com os seguintes atributos:
ano (inteiro, permite nulo, default 0).preco (decimal, máximo 7 dígitos, 2 casas decimais, permite nulo, default 0).modelo (chave estrangeira para Modelo).cor (chave estrangeira para Cor).acessorios (chave estrangeira para Acessorio, muitos para muitos).__str__ (retorna o id, modelo, cor e ano do carro).Veiculo.Ao final, o diagrama no arquivo core.png, que é obrigatório, deve ficar assim:

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:
Check for Updates.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:
.env) Extensão Vue.js devtools no Google Chrome
Tema de cores
Utilizo o tema de cores Escuro + do VS Code. Dẽ preferência, utilize este tema, pois facilita na visualização do erros no seu código.
Para alterar o tema de cores, useo atalho Ctrl + K e depois Ctrl + T.
Você pode configurar a sincronização das extensões entre os computadores. Para isso:
Ativar a Sincronização de Configurações.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.
Abra um terminal:
Ctrl + Alt + T
Verifique se o PDM está instalado:
pdm -V
curl -sSLv https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
Ctrl + D) e abra um novo terminal (Ctrl + Alt + T).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 -sSLv 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
.venvno diretório do projeto. Para resolver isso, você deve apagar a pasta.venve executar o comandopdm config python.use_venv falsee então executar o comandopdm install.
Se precisar instalar o Python:
sudo apt install python-is-python3 python3.10-venv
Voltar para a preparação do ambiente
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:
Create a new organization.South America (São Paulo).Configurando o banco de dados no projeto
Connect (Conectar), ao lado do nome do projeto.Session Pooler.
postgresql://postgres.kfjxquvsjldesrrjqgzo:[YOUR-PASSWORD]@aws-0-sa-east-1.pooler.supabase.com:5432/postgres[YOUR-PASSWORD]..env do projeto, como no exemplo:# Supabase
DATABASE_URL=postgresql://postgres.kfjxquvsjldesrrjqgzo:senha123@aws-0-sa-east-1.pooler.supabase.com:5432/postgres
Migrando o banco de dados
.env:
DATABASE_URL.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.
Supabase, acesse o Table Editor e verifique que as tabelas foram criadas.Database, Schema Visualizer.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:
.env:
DATABASE_URL.IMPORTANTE: A cada nova alteração no banco de dados, você deve repetir este processo de migração, tanto no banco de dados local quanto no banco de dados do Supabase.
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).
build.sh na raiz do projeto.Testando a execução localmente
pdm run python -m gunicorn app.asgi:application -k uvicorn.workers.UvicornWorker
http://localhost:8000 no navegador para verificar se a aplicação está funcionando.O que fizemos foi substituir o servidor de desenvolvimento do Django pelo servidor
UvicorneGunicorn.
Configurando o Render
Build and deploy from a Git repository (Construir e implantar a partir de um repositório Git).nome-do-projeto.Ohio (US East).main.Python../build.sh.python -m gunicorn app.asgi:application -k uvicorn.workers.UvicornWorker.Free.Add from .env e adicione as informações do seu arquivo .env: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_KEYnova. 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].
Create Web Service.Se tudo estiver correto, o projeto será implantado no Render.
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
.env, incluindo a seguinte variável:# 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.
Render (ou no serviço de hospedagem que você estiver utilizando), na opção Environment variables.Testando
MODE com o valor MIGRATE no arquivo .env.Admin do Django e verifique se ela foi salva no Cloudinary, na opção Media Explorer.Cloudinary para armazenar os arquivos estáticos.feat: adicionando Cloudinary
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.
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
.venv, e não a pasta __pypackages__, remova a pasta .venv:rm -rf .venv
pdm config python.use_venv false
pdm install novamente.pdm run dev novamente.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.
.env, execute o comando: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.
db.sqlite3 do projeto.settings.py:from datetime import timedelta
...
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=180),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
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
Control+Shift+G). Depois, clique no botão Initialize repository.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 e-mail.
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/
Seguem abaixo alguns comandos úteis para serem executados no Django Shell:
Criar um objeto:
from core.models import Categoria
c = Categoria(descricao='Teste')
c.save()
Listar todos os objetos:
Categoria.objects.all()
Listar um objeto específico:
Categoria.objects.get(id=1)
Atualizar um objeto:
c = Categoria.objects.get(id=1)
c.descricao = 'Teste 2'
c.save()
Deletar um objeto:
c = Categoria.objects.get(id=1)
c.delete()
Listar todos os livros com preço igual a zero:
from core.models import Livro
Livro.objects.filter(preco=10)
Mostrar a quantidade de livros com preço igual a zero:
Livro.objects.filter(preco=0).count()
ou
len(Livro.objects.filter(preco=0))
Alterar o preço de todos os livros com preço igual a zero:
Livro.objects.filter(preco=0).update(preco=10)
Listar todos os livros com preço nulo:
Livro.objects.filter(preco__isnull=True)
Alterar a editora de todos os livros de um editora específica:
for livro in Editora.objects.get(id=167).livros.all():
livro.editora_id = 11
livro.save()
Listar todos os livros de uma categoria específica (usando o atributo related_name):
Categoria.objects.get(descricao='Comédia').livros.all()
Listar todos os livros de uma categoria específica (usando o atributo categoria):
Livro.objects.filter(categoria__descricao='Comédia')
Remover todas as categorias que não possuem livros:
for categoria in Categoria.objects.all():
if len(categoria.livros.all()) == 0:
print(categoria)
categoria.delete()
Ajustar o preço do item de compra com base no preco do livro se o preço do item de compra estiver zerado.
from compras.models import ItensCompra
for item in ItensCompra.objects.filter(preco=0):
item.preco = item.livro.preco
item.save()
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;
Os 12 Fatores são princípios criados pela equipe da Heroku para o desenvolvimento de aplicações modernas, escaláveis e prontas para a nuvem. Eles ajudam a manter o código limpo, a implantação simples e a aplicação resiliente. Abaixo, explicamos cada um deles, aplicando diretamente ao nosso projeto.
| Para maiores informações, assista ao vídeo [A Forma Ideal de Projetos Web | Os 12 Fatores](https://www.youtube.com/watch?v=gpJgtED36U4) de Fábio Akita ou acesse o site 12factors.net. A documentação em português pode ser encontrada aqui. |
1. Código-base – Uma base de código por aplicação Uma aplicação deve ter uma única base de código, versionada em um sistema de controle de versão (ex: Git). O código deve ser separado do ambiente de execução.
Nosso projeto backend Django/DRF está em um repositório GitHub, separado do frontend Vue.js, também versionado no Git. Ambos seguem o princípio de um repositório por código-base, facilitando controle, versionamento e CI/CD.
2. Dependências – Declare e isole as dependências As dependências devem ser declaradas explicitamente e isoladas do sistema. Isso garante que a aplicação funcione em qualquer ambiente.
No backend, usamos o PDM com o pyproject.toml para declarar pacotes como Django, DRF, passage.id, etc. No frontend, usamos package.json com Pinia, Axios e Vue. Assim, qualquer ambiente pode reproduzir o mesmo setup com pdm install ou npm install.
3. Configurações – Armazene as configurações no ambiente
As configurações devem ser armazenadas como variáveis de ambiente, separadas do código. Isso permite que a aplicação funcione em diferentes ambientes (dev, test, stage, prod) sem alterações no código.
As configurações são armazenadas em um arquivo .env, que não é versionado. O Django usa django-environ para carregar variáveis do .env, como DATABASE_URL, SECRET_KEY, DEBUG, etc. O Vue.js utiliza o plugin dotenv para carregar variáveis prefixadas com VITE_. Assim, as configurações são mantidas fora do código-fonte e podem ser alteradas facilmente.
4. Serviços de Apoio – Trate serviços de apoio como recursos anexos
Serviços externos como banco de dados ou armazenamento devem ser tratados como recursos externos e facilmente substituíveis. O projeto usa PostgreSQL no Supabase e Cloudinary para armazenamento de imagens. O Vue.js consome a API do Django, que se conecta ao banco de dados. O passage.id é usado para autenticação. Todos esses serviços são configurados via variáveis de ambiente, permitindo fácil troca entre ambientes. Nosso app pode usar SQLite localmente e PostgreSQL na produção, sem alterar o código.
5. Build, Release, Run – Separe os estágios de build e execução
A aplicação deve ter um processo claro de build, release e run. O build prepara o código, o release configura o ambiente e o run executa a aplicação.
No Django, fazemos pdm install (build), configuramos variáveis (release) e rodamos pdm run dev ou Gunicorn (run). O frontend Vue é empacotado com npm run build e serve arquivos estáticos via Render.
6. Processos – Execute a aplicação como um ou mais processos stateless
A aplicação deve ser executada como um ou mais processos independentes, sem estado. Isso permite escalar horizontalmente e reiniciar processos sem perda de dados.
O Django é executado com Gunicorn, que inicia múltiplos workers. O Vue.js é uma SPA, servida como arquivos estáticos. Ambos não mantêm estado entre requisições. O estado é gerenciado no frontend (Vuex) ou via tokens JWT. Isso permite escalar horizontalmente e reiniciar processos sem perda de dados.
7. Vínculo com Portas – Exporte serviços via binding de porta
A aplicação deve se comunicar através de portas bem definidas, permitindo que serviços externos acessem a aplicação.
O backend Django é exposto via porta definida por PORT, compatível com o Render. O frontend Vue se comunica com o backend via Axios, apontando para a URL da API configurada em tempo de build.
8. Concorrência – Escale por processo
Aplicações devem ser escaláveis através da execução de múltiplos processos idênticos.
Podemos escalar horizontalmente a API com múltiplos workers Gunicorn. O frontend Vue pode ser replicado em várias instâncias no Render, atendendo a múltiplos usuários simultaneamente.
9. Descartabilidade – Maximize a robustez com inicialização e desligamento rápidos Processos devem ser iniciados e parados rapidamente, permitindo fácil escalabilidade e recuperação de falhas.
Nosso app inicia com pdm run dev em segundos, e pode ser reiniciado sem perda de dados. O frontend Vue também é estático, com build e deploy rápidos.
10. Paridade entre Ambientes – Mantenha desenvolvimento, staging e produção o mais similares possível
Ambientes de desenvolvimento, staging e produção devem ser o mais semelhantes possível para evitar problemas de compatibilidade.
A diferença principal entre dev e produção é o banco (SQLite vs PostgreSQL), mas toda a configuração é mantida via .env. Com isso, conseguimos boa paridade entre ambientes.
11. Logs – Trate logs como fluxo de eventos
Os logs devem ser emitidos para stdout/stderr e tratados como fluxo contínuo
Os logs do Django são enviados para o console, permitindo fácil monitoramento. No Render, os logs são capturados automaticamente. O Vue.js registra mensagens importantes no console para debug, facilitando a identificação de problemas.
12. Processos Administrativos – Execute tarefas admin como processos pontuais
Tarefas como migrações ou comandos de manutenção devem ser executadas como processos avulsos.
Usamos comandos como pdm run migrate, createsuperuser ou shell_plus para tarefas administrativas. No Vue.js, comandos de build e lint também são pontuais.
Conclusão Nosso projeto Django + Vue.js segue os 12 fatores de forma consistente, o que nos permite ter uma aplicação modular, escalável, fácil de manter e com deploy contínuo. Essas boas práticas são fundamentais para garantir qualidade e estabilidade tanto em desenvolvimento quanto em produção.
O django-extensions traz o comando runserver_plus, que permite iniciar o servidor de desenvolvimento do Django com SSL (HTTPS). Isso é útil quando você precisa testar recursos que exigem HTTPS, como autenticação via OAuth2, cookies Secure ou APIs que só aceitam conexões seguras (como Spotify, por exemplo).
Primeiro, instale os pacotes necessários:
pdm add django-extensions werkzeug pyOpenSSL
runserver_plus.Você pode rodar o servidor com um certificado autoassinado de forma bem simples:
pdm run python manage.py runserver_plus --cert-file cert.pem
Se o arquivo cert.pem não existir, o Django Extensions irá gerar automaticamente um certificado e uma chave, armazenando tudo em cert.pem.
pyproject.tomlPara não ter que digitar o comando completo toda vez, adicione um script no seu pyproject.toml:
[tool.pdm.scripts]
devssl = "python manage.py runserver_plus --cert-file cert.pem"
Agora você pode rodar com:
pdm devssl
Pronto! Agora seu projeto Django pode ser testado com HTTPS de maneira simples durante o desenvolvimento.
Para contribuir com este projeto:
Marco André Mendes <marcoandre@gmail.com>