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:
- Programação I (Prof. Fábio Longo de Moura): Lógica de Programação com JavaScript.
- Desenvolvimento Web II (Prof. Eduardo da Silva): Desenvolvimento front-end com VueJS.
- Desenvolvimento Dispositivos Móveis III (Prof. Eduardo da Silva): Desenvolvimento para dispositivos móveis com Vue + Vite + PWA.
- Desenvolvimento Web III - Atual (Prof. Marco André Lopes Mendes): Desenvolvimento back-end com Django e DRF, utilizando o modelo de projeto.
- Desenvolvimento Web III (2023) (Prof. Marco André Lopes Mendes): Desenvolvimento back-end com Django e DRF, do zero, sem utilizar o template.
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.
- Instale ou atualize o VS Code
- Instale e sincronize as extensões do VS Code.
- Instale e configure o PDM
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
livraria
a partir de um repositório de template. Se você quiser criar aprender a criar um projeto do zero, acesse o tutorial de 2023.
- Acesse o template em https://github.com/marrcandre/template_django_pdm.
- Clique no botão
Use this template
emCreate a new repository
. - Preencha as informações solicitadas:
Owner
: <seu usuário no GitHub>Repository name
:livraria
- Click no botão
Create repository
.
Feito isso, o repositório
livraria
será criado no seu GitHub.
2.3 Clonando o projeto
Você pode clonar o projeto de duas formas:
2.3.1 Usando o VS Code
- Abra o VS Code.
- Clique no ícone de Source Control na barra lateral esquerda.
- Clique no botão
Clone Repository
. - Você também pode teclar
Control+Shift+P
e digitarClone Repository
.
- Clique no botão
- Digite a URL do repositório do projeto (ou procure na lista de repositórios disponíveis).
- Escolha a pasta onde o projeto será clonado.
- Clique no botão
Clone
.
2.3.2 Usando o terminal
- Abra o terminal.
- Vá para a pasta onde o projeto será clonado.
- Digite o comando:
git clone <URL do repositório>
- Abra o projeto no VS Code, digitando:
code .
O projeto criado ficará assim:
2.4 Instalando as dependências
- Abra o terminal no VS Code (Ctrl+Shift+´).
- Instale as dependências do projeto:
pdm install
2.5 Criando o arquivo .env
- Crie o arquivo
.env
, a partir do arquivo.env.exemplo
: - Abra o arquivo
.env.exemplo
. - Escolha a opção
Salvar como...
(Ctrl+Shift+S). - Salve o arquivo como
.env
.
Opcionalmente, você pode criar o arquivo
.env
a partir do terminal, digitando:
cp .env.exemplo .env
2.4 Rodando o servidor de desenvolvimento
- Para executar o projeto, digite no terminal:
pdm run dev
2.5 Acessando o projeto
-
Acesse o projeto no navegador:
-
Os dados de acesso são:
- Usuário:
[email protected]
- Senha:
teste.123
- Usuário:
-
Após acessar, você pode o nome do usuário e a senha.
IMPORTANTE: O servidor de desenvolvimento deve estar sempre rodando para que o projeto funcione.
É isso! Seu projeto está inicializado e rodando!!!
2.6 Exercício
- Apague o projeto e crie novamente, seguindo as instruções acima.
- Verifique se o projeto está rodando e se o
Admin
está em execução. - Observe que configurações precisam ser feitas novamente e quais não foram mais necessárias.
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.py
do 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 asmodels
(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 doAdmin
, 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
models
já 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. -
Dentro da pasta
models
da aplicaçãocore
crie um arquivo chamadocategoria.py
. -
Adicione o seguinte código no arquivo
categoria.py
:
from django.db import models
class Categoria(models.Model):
descricao = models.CharField(max_length=100)
Nesse código, você:
-
Importou o pacote necessário para criar a
model
; -
Criou a classe
Categoria
; -
Incluiu o campo
descricao
, que é umastring
de no máximo 100 caracteres. Esse campo é obrigatório. -
IMPORTANTE:
- O nome da classe deve ser sempre no singular e com a primeira letra maiúscula.
- O nome dos campos deve ser sempre no singular e com a primeira letra minúscula.
3.4 Inclusão da model
no arquivo __init__.py
- Precisamos ainda incluir a
model
no arquivo__init__.py
da pastamodels
:
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.
- Acesse o arquivo do banco de dados (
db.sqlite3
) e verifique se a tabelacore_categoria
foi criada. - Para ver o diagrama de classes atualizado, acesse o arquivo
core.png
na pasta raiz do projeto. - Acesse o
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
.
- Vamos incluir a
model
noAdmin
. Abra o arquivoadmin.py
da aplicaçãocore
e adicione o seguinte código no final do arquivo:
admin.site.register(models.Categoria)
3.7 Exercício
- Acesse novamente o
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
- Inclua algumas categorias no banco de dados.
- Você perceberá que a descrição das informações que você inclui está meio estranha, algo como
Categoria object (1)
e assim por diante. - Para resolver, isso, vamos fazer uma pequena modificação na
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.
- Vamos incluir o método
__str__
namodel
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
- Verifique antes se seu computador está configurado corretamente para o git com as suas credenciais. Veja como fazer isso aqui.
- Faça um commit com a mensagem
feat: criação da model de Categoria
.
IMPORTANTE: Escrevendo uma boa mensagem de commit
- Escreva uma mensagem de commit que descreva o que foi feito.
- Dessa forma fica mais fácil identificar as mudanças sem precisar ver o código.
- Não escreva mensagens como
Alteração 1
,Alteração 2
,Alteração 3
, etc. - Utilize prefixos:
- feat: Para novas funcionalidades.
- fix: Para correções de bugs.
- chore: Para tarefas de manutenção ou mudanças que não afetam o código de produção.
- docs: Para alterações na documentação.
- style: Para mudanças de formatação ou estilo de código (sem alterar lógica).
- refactor: Para refatoração de código sem adicionar funcionalidades ou corrigir bugs.
- test: Para adição ou modificação de testes.
- Exempos de commits:
- feat: Criação da model de Categoria
- fix: Correção do método str da model Categoria
- chore: Atualização do README.md
- docs: Adição de comentários no código
- style: Alteração de formatação do código
- refactor: Refatoração do código da model Categoria
- test: Adição de testes para a model Categoria
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)
- Observe que o
DRF
já está instalado no projeto, conforme os arquivospyproject.toml
erequirements.txt
. - Além disso, o
DRF
já está configurado no arquivosettings.py
, na seçãoINSTALLED_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."
- Crie o arquivo
categoria.py
na pastaserializers
da aplicaçãocore
, e adicione o seguinte código, para criar aCategoriaSerializer
:
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
- Inclua o serializer no arquivo
__init__.py
da pastaserializers
:
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.
- Crie a view
CategoriaViewSet
na pastaviews
da aplicaçãocore
, no arquivocategoria.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
- Inclua a view no arquivo
__init__.py
da pastaviews
:
from .categoria import CategoriaViewSet
4.4 Criação das rotas (urls)
As rotas são responsáveis por mapear as URLs
para as views
.
- Para criar as rotas da
Categoria
, edite o arquivourls.py
na pastaapp
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:
Se tudo correu bem, você deve ver a interface do DRF.
- Você pode acessar diretamente a rota da
Categoria
: http://0.0.0.0:19003/api/categorias/
Isso deve trazer todas as categorias do banco, no formato JSON.
- Para acessar um único registro, use o seguinte formato: http://0.0.0.0:19003/api/categorias/1/
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:
- GET para listar todos os registros: http://0.0.0.0:19003/api/categorias/
- GET para listar apenas 1 registro: http://0.0.0.0:19003/api/categorias/1/
- POST (para criar um novo registro): http://0.0.0.0:19003/api/categorias/
- PUT (para alterar um registro existente): http://0.0.0.0:19003/api/categorias/1/
- PATCH (para alterar parcialmente um registro): http://0.0.0.0:19003/api/categorias/1/
- DELETE (para remover um registro): http://0.0.0.0:19003/api/categorias/1/
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:
- Thunder Client (extensão do VS Code)
- RapidAPI (extensão do VS Code)
- Insomnia (externo)
- Postman (externo)
4.8 Utilizando o Swagger
O Swagger é uma ferramenta que permite a documentação e teste de APIs.
-
Para acessar o Swagger, acesse:
4.9 Exercícios: testando a API e as ferramentas
Instale uma ou mais das ferramentas sugeridas.
- Experimente as seguintes tarefas:
- Criar uma ou mais categorias;
- Listar todas as categorias;
- Alterar uma ou mais categorias, utilizando PUT e PATCH;
- Listar a categoria alterada;
- Remover uma categoria;
- Incluir outra categoria;
- Listar todas as categorias.
4.10 Fazendo um commit
- Faça um commit com a mensagem
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.
- Entre no repositório do template: https://github.com/marrcandre/template-vue3.
- Clique no botão
Use this template
emCreate a new repository
. - Clone o projeto para o seu computador.
- Execute os seguintes comandos:
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
- Os passos para a criação da API para a classe
Editora
são os mesmos que fizemos para a classeCategoria
:- Criar a
model
Editora no arquivoeditora.py
na pastamodels
. - Incluir a
model
no arquivo__init__.py
da pastamodels
. - Fazer a migração.
- Registrar a
model
no arquivoadmin.py
. - Criar o serializador no arquivo
editora.py
na pastaserializers
. - Incluir o serializador no arquivo
__init__.py
da pastaserializers
. - Criar a viewset no arquivo
editora.py
na pastaviews
. - Incluir a
viewset
no arquivo__init__.py
da pastaviews
. - Incluir a nova rota em
urls.py
.
- Criar a
6.2 Criação e modificação dos arquivos
- Os arquivos ficarão assim:
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
- Faça a migração e efetive a migração:
pdm run migrate
- Verifique se a tabela
core_editora
foi criada no banco de dados.
6.4 Exercícios: testando da API da Editora
- Acesse o endpoint: http://0.0.0.0:19003/api/editoras/
- Teste todas as operações da
Editora
. - Verifique se é possível incluir novas editoras sem incluir todos os campos.
- Tente utilizar o PUT e o PATCH sem informar todos os campos.
6.5 Fazendo um commit
- Faça um commit com a mensagem
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
.
- Crie a API para a classe
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. -
Teste a API.
-
Faça o commit, com a mensagem
feat: criação da API para Autor
.
Exercícios:
- Crie no Vuejs a tela para listar, incluir, alterar e excluir autores.
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
- Vamos criar o modelo de dados
Livro
, no arquivomodels/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, null=True, blank=True)
def __str__(self):
return f'({self.id}) {self.titulo} ({self.quantidade})'
Inclua o modelo no arquivo __init__.py
da pasta models
:
from .livro import Livro
- Faça as migrações e veja o resultado no banco de dados.
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.
- Após a criação da API, teste todas as operações de CRUD para a classe
Livro
. - Faça um commit com a mensagem
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
- Inclua a linha a seguir no modelo
Livro
, logo após o atributopreco
:
from .categoria import Categoria
...
categoria = models.ForeignKey(
Categoria, on_delete=models.PROTECT, related_name='livros', null=True, blank=True
)
...
- Vamos entender cada parte:
models.ForeignKey
: define o campo como sendo uma chave estrangeira.Categoria
: omodel
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 classeCategoria
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.- Na prática, juntos eles permitem que o campo seja não obrigatório.
- Isso é útil para evitar problemas na migração.
9.2 Campo editora
no Livro
- De forma semelhante, vamos associar o livro a uma editora, incluindo logo em seguida à categoria, a seguinte linha:
from .editora import Editora
...
editora = models.ForeignKey(Editora, on_delete=models.PROTECT, related_name='livros', null=True, blank=True)
- Faça a migração dos dados.
Observe que os campos
categoria_id
eeditora_id
foram criados no banco de dados, na tabelacore_livro
. Eles são os campos que fazem referência às tabelascore_categoria
ecore_editora
.
A model Livro
ficará assim:
9.3 Testando o atributo on_delete
Feito isso, verifique se tudo funcionou.
No Admin
:
- Cadastre algumas categorias, editoras, autores e livros.
- Note como os livros acessam as categorias e editoras já cadastradas.
- Tente apagar uma editora ou categoria com livros associados.
- O que aconteceu?
- Por que isso aconteceu?
- Tente apagar uma editora ou categoria sem livros associados.
- O que aconteceu?
- Por que isso aconteceu?
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:
- Abra o Django shell:
pdm run shellp
- Acesse os livros da categoria com
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.
- Faça um commit com a mensagem
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.).
- Inclua o campo
autores
no modeloLivro
:
from .autor import Autor
...
autores = models.ManyToManyField(Autor, related_name='livros', blank=True)
...
- Execute as migrações.
Observe que o campo
autores
não foi criado na tabelacore_livro
. Ao invés disso, uma tabela associativa foi criada, com o nomecore_livro_autores
, contendo os camposlivro_id
eautor_id
. É assim que é feito um relacionamento n para n no Django.
Nesse caso, não é necessário usar o atributo
null=True
eblank=True
, pois um campo do tipoManyToManyField
cria uma tabela associativa.
- A model
Livro
ficará assim:
Note que na ligação entre
Livro
eAutor
existem uma "bolinha" em cada lado, indicando que o relacionamento é n para n.
Já no caso de
Livro
comCategoria
eEditora
, existe uma "bolinha" emLivro
e um "pino" emCategoria
eEditora
, indicando que o relacionamento é n para 1.
Observe as alterações no banco de dados, no Admin e na API.
- Faça um commit com a mensagem
feat: inclusão do relacionamento n para n entre Livro e Autor
.
10.2 Exercícios
- Teste a API REST de livros com modificações feitas.
- Faça o Exercício da Garagem (E1) para praticar o que foi aprendido até aqui.
-
Acesse a API do Livro e veja como está a apresentação dos autores:
Observou que no
Livro
, aparecem apenas os camposid
da categoria, da editora e dos autores e não as descrições?
- Vamos resolver isso.
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).
- Para resolver isso, vamos criar dois (ou mais) serializadores, sendo um para a listagem e outro para a recuperação de um único livro:
class LivroSerializer(ModelSerializer):
class Meta:
model = Livro
fields = '__all__'
class LivroListRetrieveSerializer(ModelSerializer):
class Meta:
model = Livro
fields = '__all__'
depth = 1
- Inclua o serializador
LivroListRetrieveSerializer
no arquivoserializers/__init__.py
:
from .livro import LivroListRetrieveSerializer, LivroSerializer
Observe que no
LivroListRetrieveSerializer
foi incluído o atributodepth = 1
, que permite a apresentação dos dados relacionados.
- Na viewset, escolhemos o serializador conforme a operação:
...
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.
- Teste a API.
- Faça um commit com a mensagem
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.
- Inclua um serializador
LivroListSerializer
para a listagem de livros, que mostre apenas oid
, otítulo
e opreço
e renomeie o serializadorLivroListRetrieveSerializer
paraLivroRetrieveSerializer
:
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
- Altere a viewset para utilizar este serializador na listagem:
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.
- Não eaqueça de atualizar o arquivo
serializers/__init__.py
:
from .livro import LivroListSerializer, LivroRetrieveSerializer, LivroSerializer
- Teste a API. Observe que a listagem de vários livros está diferente da recuperação de um único livro.
- Faça um commit com a mensagem
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.
- No
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
- No
Windows
, execute os seguintes comandos noPowerShell
:
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
- Remova a pasta
__pypackages__
e o arquivopdm.lock
:
rm -rf __pypackages__ pdm.lock
- Recrie o arquivo
pdm.lock
:
pdm lock
- Instale as dependências:
pdm install
Registro da app
- Adicione o pacote
uploader
na lista deINSTALLED_APPS
, nosettings.py
:
INSTALLED_APPS = [
...
'uploader', # nova linha
'core',
...
]
IMPORTANTE: Não esqueça da vírgula no final da linha.
Configuração no urls.py
- Inclua o seguinte conteúdo no arquivo
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)
...
- Modifique a linha
post_migrate
no arquivopyproject.toml
para incluir a geração do diagrama da appuploader
:
post_migrate = "python manage.py graph_models --disable-sort-fields -S -g -o core.png core uploader"
Migração do banco de dados
- Faça a migração do banco de dados:
pdm run migrate
- Se o seu projeto já foi publicado, não esqueça de fazer a migração também no servidor.
Uso em modelos
Agora que a aplicação uploader
está configurada, vamos utilizá-la para associar imagens aos livros.
- Edite o arquivo
models/livro.py
da aplicaçãolivraria
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_NULL
indica que, ao apagar a imagem, o campocapa
será setado comoNULL
.
- Faça novamente a migração do banco de dados:
pdm run migrate
O modelo Livro
ficará assim:
Observe que o campo
capa_id
foi criado na tabelacore_livro
, fazendo referência à tabelauploader_image
.
Uso no serializer
- Edite o arquivo
serializers/livro.py
da aplicaçãocore
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:
-
Faça o upload de uma imagem.
-
Observe que o campo
capa_attachment_key
foi preenchido com o valorattachment_key
da imagem. -
Guarde o valor do campo
capa_attachment_key
. -
Crie um novo livro, preenchendo o campo
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. -
Faça um commit com a mensagem
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:
- Link:
http://191.52.55.236:19003/admin/
(ou peça ao professor) - Usuário:
[email protected]
- Senha:
teste.123
- Link:
-
Cadastre pelos menos 10 livros, com autor e editora
-
Verifique se o livro, categoria, autor ou editora já estão cadastrados, para evitar duplicidade.
-
NÃO USE CAIXA ALTA!!!
-
Use o formato de nomes de livros, como no exemplo:
O Senhor dos Anéis - A Sociedade do Anel
Cópia de segurança dos dados
- Execute o comando
dumpdata
:
pdm run dumpdata > core.json
- Observe que o arquivo
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:
- Baixe o arquivo
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
- Execute o comando
loaddata
:
pdm run loaddata
O comando espera um arquivo
core.json
na 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
.
- Edite o arquivo
models/editora.py
e adicione os camposemail
ecidade
:
class Editora(models.Model):
...
email = models.EmailField(max_length=100, blank=True, null=True)
cidade = models.CharField(max_length=100, blank=True, null=True)
- Faça a migração dos dados e tente fazer o "load" novamente:
Verificando se a carga dos dados funcionou
- Utilizando o Django Shell Plus, observe que os dados foram carregados:
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.
- Edite o arquivo
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):
...
- A linha
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
- As linhas com
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: -
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.
- Acesse o shell:
pdm run shellp
- Crie um objeto:
>>> categoria = Categoria.objects.create(descricao='Desenvolvimento Web')
- Observe que o objeto foi criado:
>>> categoria
<Categoria: Desenvolvimento Web>
- Liste os objetos:
>>> Categoria.objects.all()
<QuerySet [<Categoria: Desenvolvimento Web>]>
- Obtenha o objeto:
>>> categoria = Categoria.objects.get(descricao='Desenvolvimento Web')
- Observe que o objeto foi obtido:
>>> categoria
<Categoria: Desenvolvimento Web>
- Atualize o objeto:
>>> categoria.descricao = 'Desenvolvimento Web com Django'
>>> categoria.save()
- Observe que o objeto foi atualizado:
>>> categoria
<Categoria: Desenvolvimento Web com Django>
- Remova o objeto:
>>> categoria.delete()
(1, {'core.Categoria': 1})
- Observe que o objeto foi removido:
>>> Categoria.objects.all()
<QuerySet []>
Usando o atributo related_name
- Acesso a todos os livros de um autor:
Autor.objects.get(id=1).livros.all()
- Acesso a todos os livros de uma categoria:
Categoria.objects.get(id=1).livros.all()
- Acesso a todos os livros de uma editora:
Editora.objects.get(id=1).livros.all()
- Encerre o shell:
>>> 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 é:
- Criar grupos para perfis de usuários específicos.
- Definir as permissões que este grupo de usuários terá.
- Criar um usuário para cada pessoa que utilizará a aplicação.
- Incluir os usuários nos grupos, dando assim as permissões.
- No caso de mudanças nas permissões, elas são sempre feitas nos grupos, refletindo nos usuários.
- Se um usuário possui mais do que um perfil de permissões, ele deve ser incluído em vários grupos.
- Quando um usuário sai de uma função ou deve perder seus privilégios, ele é removido do grupo específico.
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:
- Crie um grupo chamado
administradores
, com as seguintes as permissões:- Adicionar, editar, visualizar e remover:
autor
,categoria
eeditora
. - Adicionar, editar e visualizar:
livro
.
- Adicionar, editar, visualizar e remover:
- Crie um grupo chamado
compradores
, com as seguintes permissões:- Visualizar:
autor
,categoria
eeditora
. - Adicionar, editar e visualizar:
livro
.
- Visualizar:
As permissões para compradores
devem ficar assim:
b. Criando usuários e adicionando aos grupos
- Crie um usuário
[email protected]
e o inclua no grupoAdministradores
. - Crie um usuário
[email protected]
e o inclua no grupoCompradores
.
c. Testando as permissões
- Acesse o
Admin
com o usuário[email protected]
e verifique se ele tem acesso a todas as permissões do grupoAdministradores
.- Ele deve conseguir adicionar, editar, visualizar e remover
autor
,categoria
,editora
. - Deve também conseguir adicionar, editar e visualizar
livro
(mas não deve conseguir removerlivro
).
- Ele deve conseguir adicionar, editar, visualizar e remover
- Acesse o
Admin
com o usuário[email protected]
e verifique se ele tem acesso apenas às permissões do grupoCompradores
.- Ele deve conseguir apenas visualizar
autor
,categoria
eeditora
, sem alterar ou excluir esses objetos. - Ele deve também conseguir adicionar, editar e visualizar
livro
, mas não deve conseguir excluir livros.
- Ele deve conseguir apenas visualizar
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:
- a nível de objeto (nas
views
ouviewsets
, por exemplo); - de forma global, no arquivo
settings.py
; - com o uso de classes de permissão do
Django REST Framework
.
Vamos analisar cada uma dessas formas.
a. Exemplo de uso de permissão na viewset
Vamos ver um exemplo de uso de permissão em uma viewset
. No exemplo, vamos permitir acesso apenas a usuários autenticados na model Categoria
.
Como ilustração, modifique o arquivo views/categoria.py
, da seguinte forma.
- Importe a seguinte função:
from rest_framework.permissions import IsAuthenticated
- Inclua também a seguinte linha na
CategoriaViewSet
:
permission_classes = [IsAuthenticated]
Para testar:
- Encerre a sessão do Admin.
- Tente acessar as categorias pelo DRF.
- Você deve receber o seguinte erro:
"As credenciais de autenticação não foram fornecidas."
- Agora entre novamente pelo Admin.
- Tente acessar as categorias pelo DRF.
- Você deve conseguir acessar novamente.
Resumindo, utilizamos a classe
IsAuthenticated
para permitir acesso apenas a usuários autenticados.
b. Exemplo de uso de permissão no settings.py
Outra forma de gerenciamento de permissões é feita no arquivo settings.py
.
IMPORTANTE: Para utilizá-la, comente as últimas alterações feitas no arquivo
views/categoria.py
.
Uma forma de conseguir o mesmo resultado de forma padrão para todo o projeto, isto é, permitir acesso aos endpoints apenas para usuários autenticados, é configurar desse modo o arquivo settings.py
:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Para testar:
- Inclua o código acima e teste novamente o acesso aos endpoints do DRF (categorias, editoras, etc.) com e sem uma sessão autenticada.
Resumindo, utilizamos a classe
IsAuthenticated
nosettings.py
para permitir acesso apenas a usuários autenticados.
c. Permissões com o DjangoModelPermissionsOrAnonReadOnly
Apesar de ser possível definir a autorização das formas que vimos anteriormente, adotaremos uma outra forma. Essa forma que iremos adotar para o gerenciamento de permissões será com o uso do DjangoModelPermissions.
Esta classe de permissão está ligada às permissões do modelo django.contrib.auth
padrão do Django. Essa permissão deve ser aplicada apenas a visualizações que tenham uma propriedade .queryset
ou método get_queryset()
(exatamente o que temos).
A autorização só será concedida se o usuário estiver autenticado e tiver as permissões de modelo relevantes atribuídas, da seguinte forma:
- As solicitações
POST
exigem que o usuário tenha a permissão de adição (add
) no modelo. - As solicitações
PUT
ePATCH
exigem que o usuário tenha a permissão de alteração (change
) no modelo. - As solicitações
DELETE
exigem que o usuário tenha a permissão de exclusão (remove
) no modelo. - Se o usuário não estiver autenticado, ele terá acesso somente leitura à API.
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:
- Crie uma conta em https://passage.id/.
- Clique em
Login
e depois emRegistre-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:
- Clique em
Create App
. - Escolha a opção
Passkey complete
e clique no botãoContinue
. - Escolha a opção
Embedded login experience
e preencha os campos solicitados:Name your app
:livraria
(por exemplo)Enter the domain for your app
:http://localhost:5173
Enter the redirect URL
:/
- Clique em
Create App
para finalizar a criação do aplicativo
Importante: o domínio e a porta devem ser os mesmos que você está utilizando para desenvolver o seu PWA. No nosso caso, estamos utilizando o domínio http://localhost:5173. Quando você for colocar o seu PWA em produção, você deve alterar o domínio para o domínio do seu site.
Configuração do Passage no backend Django
- Descomente (ou inclua) as seguintes linhas no arquivo
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
- No arquivo
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
- No arquivo
models/user.py
, inclua o campofoto
:
...
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=True
eblank=True
.
O campo
foto
seránull
por padrão, por isso utilizamosdefault=None
.
Se a foto for deletada, o campo
foto
seránull
, por isso utilizamoson_delete=models.SET_NULL
.
- Faça as migrações:
Seu projeto deve ficar assim:
Observe a ligação entre a model
User
e a modelImage
, através da chave estrangeirafoto
.
Inclusão da foto no Admin
- No arquivo
admin.py
, inclua o campofoto
:
...
class UserAdmin(BaseUserAdmin):
...
(_('Personal Info'), {'fields': ('name', 'passage_id', 'foto')}),# inclua a foto aqui
...
- Teste a inclusão da foto de um usuário pelo
Admin
.
Inclusão da foto no serializer
- Substitua o serializador para o usuário, em
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=True
indica que o campofoto_attachment_key
é apenas para escrita. Isso significa que ele não será exibido na resposta da API.
O atributo
read_only=True
indica que o campofoto
é apenas para leitura. Isso significa que ele não será aceito na requisição da API.
Testando
- Inclua uma foto de perfil em um usuário, através da API.
Finalizando
- Faça as alterações no sistema publicado.
- Faça um commit com a mensagem
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
- Crie um novo arquivo
compra.py
dentro da pastamodels
do appcore
, digitando no terminal:
touch core/models/compra.py
- Inclua o seguinte conteúdo no arquivo
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, 'Realizado'
PAGO = 3, 'Pago'
ENTREGUE = 4, 'Entregue'
usuario = models.ForeignKey(User, on_delete=models.PROTECT, related_name='compras')
status = models.IntegerField(choices=StatusCompra.choices, default=StatusCompra.CARRINHO)
Note que estamos utilizando a model
User
comoForeignKey
para a modelCompra
.
StatusCompra
é do tipoIntegerChoices
, que é uma forma de criar um campochoices
com valores inteiros.
status
é um campoIntegerField
que utiliza ochoices
StatusCompra.choices
e tem o valor padrãoStatusCompra.CARRINHO
, que no caso é1
.
Opcionalmente, poderíamos ter criado uma entidade
StatusCompra
e utilizado um campoForeignKey
para ela. No entanto, como temos um número pequeno de status, optamos por utilizar oIntegerField
comchoices
.
- Inclua a nova model no arquivo
core/models/__init__.py
:
from .compra import Compra
Adicionando a model Compra
ao Admin
- Adicione a model
Compra
aoadmin.py
do appcore
:
...
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
- Execute as migrações.
O seu projeto deve ficar assim:
Testando a model Compra
- Teste a model
Compra
noAdmin
do Django.
Finalizando
- Faça um commit com a mensagem
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
.
- Vamos adicionar uma nova entidade
ItensCompra
ao arquivocore/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 oItensCompra
tenha um atributolivro
.
- Inclua o novo model no arquivo
__init__.py
dos models:
from .compra import Compra, ItensCompra
- Execute as migrações (você já sabe como fazer, certo?)
O seu projeto deve ficar assim:
- Verifique que a tabela
core_itenscompra
foi criada no banco de dados. - Inclua o model
ItensCompra
noAdmin
do Django. - Faça um commit com a mensagem
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.
- No arquivo
admin.py
do appcore
, modifique o código das modelsCompra
eItensCompra
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
Admin
do Django, você verá os itens da compra logo abaixo do formulário de edição da compra.
Opcionalmente, você pode utilizar o
StackedInline
ao invés doTabularInline
. Experimente e veja a diferença.
- Teste no
Admin
do Django. - Faça um commit com a mensagem
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
- Crie um novo arquivo
compra.py
dentro da pastaserializers
do appcore
:
touch core/serializers/compra.py
- Inclua o seguinte conteúdo no arquivo
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__'
- Inclua o novo
CompraSerializer
no arquivo__init__.py
dos serializers:
from .compra import CompraSerializer
Criação da Viewset de Compra
- Crie um novo arquivo
compra.py
dentro da pastaviews
do appcore
:
touch core/views/compra.py
- Inclua o seguinte conteúdo no arquivo
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
- Inclua o novo
CompraViewSet
no arquivo__init__.py
das views:
from .compra import CompraViewSet
URL para listagem de compras
- Inclua o endpoint no arquivo
urls.py
do appcore
:
...
from core.views import (
AutorViewSet,
CategoriaViewSet,
CompraViewSet, # inclua essa linha
EditoraViewSet,
LivroViewSet,
UserViewSet,
)
...
router.register(r'compras', CompraViewSet)
...
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- No serializer de
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
source
indica qual campo do modelCompra
será utilizado para preencher o campousuario
do serializer.
O parâmetro
read_only
indica que o campousuario
não será utilizado para atualizar o modelCompra
.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- No serializer de
Compra
, inclua o seguinte código:
...
class CompraSerializer(ModelSerializer):
status = CharField(source='get_status_display', read_only=True) # inclua essa linha
...
O parâmetro
source
indica qual método do modelCompra
será utilizado para preencher o campostatus
do serializer. Sempre que utilizamos um campo do tipoIntegerChoices
, podemos utilizar o métodoget_<nome_do_campo>_display
para obter a descrição do campo.
O parâmetro
read_only
indica que o campostatus
não será utilizado para atualizar o modelCompra
.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- Crie um serializer para
ItensCompra
, no arquivoserializers/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=True
indica que o campoitens
é uma lista de itens.
O parâmetro
read_only=True
indica que o campoitens
não será utilizado para atualizar o modelCompra
.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
feat: visualização dos itens da compra na listagem da compra
.
Mostrando os detalhes dos itens da compra na listagem de compras
- No serializer de
ItensCompra
, modifique o código:
class ItensCompraSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = '__all__'
depth = 1
O parâmetro
depth=1
indica que o serializer deve mostrar os detalhes do modelItensCompra
. O valor1
indica que o serializer deve mostrar os detalhes do modelItensCompra
e 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
, aeditora
e oautor
).
- Experimente alterar o valor de
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
.
- No
ItensCompraSerializer
, modifique a linhafields
:
fields = ('livro', 'quantidade')
O parâmetro
fields
indica quais campos do modelItensCompra
serão mostrados no serializer. Se o valor for__all__
, todos os campos serão mostrados. Se o valor for uma sequência de campos, apenas esses campos serão mostrados.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- Primeiro, importe o
SerializerMethodField
no arquivoserializers/compra.py
:
from rest_framework.serializers import CharField, ModelSerializer, SerializerMethodField
- Depois, modifique o
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
SerializerMethodField
indica que o campototal
não existe no modelItensCompra
. Ele será calculado pelo métodoget_total
.
O método
get_total
recebe como parâmetro o objetoinstance
, que representa o item da compra. A partir dele, podemos acessar os campos do item da compra, comoquantidade
elivro.preco
.
O método
get_total
retorna 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
total
no atributofields
do serializer.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- Ao final da
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
property
indica que o campototal
não existe no modelCompra
. Ele será calculado pelo métodototal
.
O método
total
retorna 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.
- Precisamos incluir o campo
total
no serializer deCompra
. NoCompraSerializer
, inclua o seguinte código:
...
fields = ('id', 'usuario', 'status', 'total', 'itens')
...
O parâmetro
fields
indica quais campos do modelCompra
serão mostrados no serializer. Se o valor for__all__
, todos os campos serão mostrados. Se o valor for uma lista de campos, apenas os campos da lista serão mostrados, na ordem da lista.
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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.
- No arquivo
admin.py
do appcore
, modifique o código da modelCompra
:
@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 doadmin
que é 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_formatado
retorna o valor do campototal
formatado como uma string, com duas casas decimais e o símbolo de real (R$).
O parâmetro
readonly_fields
indica 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_formatado
será exibido com o título "Total" na listagem doAdmin
.
O parâmetro
list_display
indica quais campos serão exibidos na listagem doAdmin
. O campototal_formatado
será exibido na listagem, com o título "Total".
- Teste o
Admin
do Django e verifique se o total da compra está sendo exibido corretamente. - Faça um commit com a mensagem
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
}
]
}
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=True
indica que o campoitens
é uma lista de itens de compra.
- Inclua também o serializer no arquivo
__init__.py
dosserializers
:
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.
- No arquivo
views/compra.py
altere oviewset
deCompra
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
- Tente criar uma nova compra usando o método
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
create
recebe 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
- Teste o endpoint no `ThunderClient.
- Faça o commit com a mensagem:
feat: criação de um endpoint para criar novas compras
Entendendo o problema
- Vamos tentar alterar uma compra existente no endpoint
compras/1/
(ou aquela que você preferir) noThunderClient
, utilizando o métodoPUT
:
{
"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
- No arquivo
serializers/compra.py
, altere oCompraCreateUpdateSerializer
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
- use o método
PUT
, para atualizar a compra de forma completa; - use o método
PATCH
, para atualizar a compra de forma parcial.- Experimente mudar apenas o usuário;
- Experimente mudar apenas a quantidade de um item da compra;
- Experimente mudar o livro de um item da compra;
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.
- No arquivo
serializers/compra.py
, crie um novo serializador chamadoCompraListSerializer
:
...
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,
)
...
- No
viewset
deCompra
, vamos alterar oserializer_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
...
- Teste o endpoint no navegador.
- Faça o commit com a mensagem
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
usuario
agora é 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 sugerido
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:
- Usuários normais vejam apenas as suas próprias compras.
- Administradores (superusuários ou membros do grupo administradores) vejam todas as compras.
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
- O método
get_queryset
é chamado sempre que o DRF precisa buscar objetos no banco de dados. - Usamos
self.request.user
para acessar o usuário autenticado. - Se o usuário for um superusuário ou pertencer ao grupo
administradores
, ele verá todas as compras. - Caso contrário, retornamos apenas as compras associadas ao próprio usuário.
Com isso, garantimos uma separação adequada de permissões entre usuários comuns e administradores.
Testando a funcionalidade
- Faça login com um usuário normal.
- Acesse o endpoint
/compras/
e confirme que apenas as compras feitas por esse usuário estão visíveis. - Agora autentique-se com um administrador (superusuário ou membro do grupo
administradores
). - Verifique se todas as compras aparecem.
Commit
Finalize com o commit:
feat: filtrando apenas as compras do usuário autenticado
Não permitindo itens com quantidade zero
Nesse momento, é possível criar uma compra com um item com quantidade zero. Vamos validar isso.
- No
serializers/compra.py
, vamos alterar o serializerItensCompraCreateUpdateSerializer
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.
- No
serializers/compra.py
, vamos alterar o serializerItensCompraCreateUpdateSerializer
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
validate
permite adicionar validações de campo que dependem de múltiplos valores ao mesmo tempo. Nesse caso, ela está verificando se a quantidade solicitada do item (item['quantidade']
) não excede a quantidade disponível em estoque (item['livro'].quantidade
).
- Para testar, tente criar uma compra com um item com a quantidade maior do que a quantidade em estoque daquele item. Você verá que a compra não é criada e é exibida uma mensagem de erro.
- Faça o commit com a mensagem
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.
- No
serializers/editora.py
, vamos alterar o serializerEditoraSerializer
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.
- Para testar, altere o e-mail de uma Editora para maiúsculas e veja que o e-mail foi gravado em minúsculas.
- Faça o commit com a mensagem
feat: formatando dados antes de salvar
.
Nesse momento, o preço do livro não é gravado no item da compra. Vamos gravar o preço do livro no item da compra, uma vez que o preço do livro pode mudar e queremos manter o registro do preço do livro no momento da compra.
Inclui o campo preco
na entidade ItensCompra
- Primeiro, precisamos incluir o campo
preco
na entidadeItensCompra
, emmodels/compra.py
:
...
class ItensCompra(models.Model):
...
preco = models.DecimalField(max_digits=10, decimal_places=2, default=0)
...
- Execute as migrações.
- Lembre-se de migrar também o banco de dados publicado, caso você esteja utilizando.
Gravando o preço do livro na criação do item da compra
- No
serializers/compra.py
, vamos alterar a funçãocreate
do serializerCompraCreateUpdateSerializer
para gravar o preço do livro no item da compra:
...
def create(self, validated_data):
itens = validated_data.pop('itens')
compra = Compra.objects.create(**validated_data)
for item in itens:
item['preco'] = item['livro'].preco # nova linha
ItensCompra.objects.create(compra=compra, **item)
compra.save()
return compra
...
O método
create
é chamado quando uma nova compra é criada. Ele recebe os dados validados e cria a compra e os itens da compra.
Alterando o campo total da compra para considerar o preço do item da compra
- Para finalizar, precisamos alterar o campo total da compra para considerar o preço do item da compra, e não o preço do livro. No
models/compra.py
, altere o métodototal
damodel
Compra
:
...
@property
def total(self):
return sum(item.preco * item.quantidade for item in self.itens.all())
...
Estamos utilizando o campo
preco
para calcular o total da compra, ao invés do campolivro.preco
.
- Para testar, crie uma nova compra e verifique que o preço do livro foi gravado no item da compra.
Gravando o preço do livro na atualização do item da compra
Da mesma forma, precisamos alterar o método update
do serializer CompraCreateUpdateSerializer
para gravar o preço do livro no item da compra:
...
def update(self, compra, validated_data):
itens = validated_data.pop('itens')
if itens:
compra.itens.all().delete()
for item in itens:
item['preco'] = item['livro'].preco # nova linha
ItensCompra.objects.create(compra=compra, **item)
compra.save()
return super().update(compra, validated_data)
...
- Para testar, altere uma compra e verifique que o preço do livro foi gravado no item da compra.
- Faça o commit com a mensagem
feat: gravando o preço do livro no item da compra
.
No momento, não existe nenhum registro da data da compra. Vamos incluir a data da compra, que será definida automaticamente no momento da criação da compra.
- Vamos incluir o campo
data
na entidadeCompra
, emmodels/compra.py
:
...
class Compra(models.Model):
...
usuario = models.ForeignKey(User, on_delete=models.PROTECT, related_name='compras')
status = models.IntegerField(choices=StatusCompra.choices, default=StatusCompra.CARRINHO)
data = models.DateTimeField(auto_now_add=True) # campo novo
...
O campo
data
é um campo do tipoDateTimeField
, que armazena a data e a hora da compra.
O parâmetro
auto_now_add=True
indica que o campo será preenchido automaticamente com a data e hora atual, quando a compra for criada.
- Execute as migrações.
Você receberá um erro na migration, pois o campo data
não pode ser nulo.
-
Escolha a opção 1, que é a opção de preencher o campo com a data atual (
timezone.now
). -
Execute as migrações no banco de dados publicado, caso você esteja utilizando.
Modificando o serializer de compra para mostrar a data da compra
Para que a data da compra seja mostrada no endpoint, precisamos modificar o serializer de Compra
para incluir o campo data
.
- No
serializers/compra.py
, vamos incluir o campodata
no serializer deCompra
:
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
...
- Para testar, crie uma nova compra e verifique que a data da compra foi gravada.
- Faça o commit com a mensagem
feat: acrescentando a data da compra
.
Vamos adicionar o tipo de pagamento à compra. O tipo de pagamento pode ser cartão de crédito
, cartão de débito
, pix
, boleto
, transferência bancária
, dinheiro
ou outro
.
- No
models\compra.py
, vamos incluir o campotipo_pagamento
no modelCompra
:
...
class Compra(models.Model):
class TipoPagamento(models.IntegerChoices):
CARTAO_CREDITO = 1, 'Cartão de Crédito'
CARTAO_DEBITO = 2, 'Cartão de Débito'
PIX = 3, 'PIX'
BOLETO = 4, 'Boleto'
TRANSFERENCIA_BANCARIA = 5, 'Transferência Bancária'
DINHEIRO = 6, 'Dinheiro'
OUTRO = 7, 'Outro'
...
tipo_pagamento = models.IntegerField(choices=TipoPagamento.choices, default=TipoPagamento.CARTAO_CREDITO)
...
O campo
tipo_pagamento
é um campo do tipoIntegerField
, que armazena o tipo de pagamento da compra. O parâmetrochoices
indica as opções de pagamento. O parâmetrodefault
indica o tipo de pagamento padrão.
-
Execute as migrações.
-
Inclua o campo
tipo_pagamento
no serializer deCompra
:
...
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âmetrosource
indica o método que retorna o tipo de pagamento.
O método
get_tipo_pagamento_display
é um método especial do model que retorna o valor legível do campotipo_pagamento
.
O campo
tipo_pagamento
foi incluído no atributofields
do serializer.
- Para testar, crie uma nova compra e verifique que o tipo de pagamento foi gravado.
- Faça o commit com a mensagem
feat: adicionando tipo de pagamento à entidade de Compra
.
No Django REST Framework (DRF), ações personalizadas são endpoints adicionais que você pode criar em uma viewset usando o decorador @action
. Elas permitem que você estenda as funcionalidades das viewsets além dos métodos RESTful padrão, como list
, retrieve
, create
, update
e destroy
. Essas ações são úteis para operações específicas que não se encaixam perfeitamente nas operações CRUD tradicionais.
Como funcionam as ações personalizadas
Ações personalizadas são métodos definidos dentro de uma viewset e decorados com @action
, que define o comportamento específico do endpoint, incluindo o verbo HTTP que será utilizado e se a ação é aplicada a um recurso específico ou a uma coleção.
Criando um serializer específico para a ação
É uma boa prática usar um serializer específico na action
ajustar_preco
. Isso traria várias vantagens, como validação mais robusta dos dados de entrada e organização do código. Ao usar um serializer dedicado, você garante que a lógica de validação e conversão dos dados está separada da view, seguindo o princípio de responsabilidade única e tornando o código mais limpo e reutilizável.
Vamos incluir um novo serializer chamado AjustarPrecoSerializer
no arquivo serializers/livro.py
:
from rest_framework.serializers import (
DecimalField,
ModelSerializer,
Serializer,
SlugRelatedField,
ValidationError,
)
...
class LivroAlterarPrecoSerializer(Serializer):
preco = DecimalField(max_digits=10, decimal_places=2)
def validate_preco(self, value):
'''Valida se o preço é um valor positivo.'''
if value <= 0:
raise ValidationError('O preço deve ser um valor positivo.')
return value
...
- Inclua o novo serializer no arquivo
__init__.py
dosserializers
:
...
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.
- No
views/livro.py
, vamos criar um métodoalterar_preco
na viewLivroViewSet
:
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from core.models import Livro
from core.serializers import (
LivroAlterarPrecoSerializer,
LivroListSerializer,
LivroRetrieveSerializer,
LivroSerializer,
)
...
@action(detail=True, methods=['patch'])
def alterar_preco(self, request, pk=None):
livro = self.get_object()
serializer = LivroAlterarPrecoSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
livro.preco = serializer.validated_data['preco']
livro.save()
return Response(
{'detail': f'Preço do livro "{livro.titulo}" atualizado para {livro.preco}.'}, status=status.HTTP_200_OK
)
O decorador
@action
cria um endpoint para a açã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 nopk
fornecido.
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
Response
retorna uma resposta HTTP.
O status
HTTP_200_OK
indica que a requisição foi bem sucedida.
-
Para testar:
- Altere o preço de um livro.
- Altere o preço de um livro com um preço inválido ou negativo.
- Altere o preço de um livro sem fornecer o preço.
-
Faça o commit com a mensagem
feat: alterando o preço de um livro
.
Criando um serializer específico para a ação
Vamos incluir um novo serializer chamado LivroAjustarEstoqueSerializer
no arquivo serializers/livro.py
:
...
class LivroAjustarEstoqueSerializer(Serializer):
quantidade = IntegerField()
def validate_quantidade(self, value):
livro = self.context.get('livro')
if livro:
nova_quantidade = livro.quantidade + value
if nova_quantidade < 0:
raise ValidationError('A quantidade em estoque não pode ser negativa.')
return value
...
- Inclua o novo serializer no arquivo
__init__.py
dosserializers
:
...
from .livro import (
LivroAjustarEstoqueSerializer, # novo
LivroAlterarPrecoSerializer,
LivroListSerializer,
LivroRetrieveSerializer,
LivroSerializer,
)
...
Criando uma ação personalizada para ajustar o estoque de um livro
Vamos criar uma ação personalizada para ajustar o estoque de um livro. Essa ação será aplicada a um recurso específico, ou seja, a um livro específico.
- No
views/livro.py
, vamos criar um métodoajustar_estoque
na viewLivroViewSet
:
@action(detail=True, methods=['post'])
def ajustar_estoque(self, request, pk=None):
livro = self.get_object()
serializer = LivroAjustarEstoqueSerializer(data=request.data, context={'livro': livro})
serializer.is_valid(raise_exception=True)
quantidade_ajuste = serializer.validated_data['quantidade']
livro.quantidade += quantidade_ajuste
livro.save()
return Response(
{'status': 'Quantidade ajustada com sucesso', 'novo_estoque': livro.quantidade}, status=status.HTTP_200_OK
)
O decorador
@action
cria um endpoint para a açãoajustar_estoque
, no formatoapi/livros/{id}/ajustar_estoque
.
- Para testar:
- Ajuste o estoque de um livro.
- Ajuste o estoque de um livro com um valor inválido.
- Ajuste o estoque de um livro sem fornecer um valor.
- Faça o commit com a mensagem
feat: ajustando o estoque de um livro
.
Nesse momento, a compra é criada com o status CARRINHO
. Vamos criar um endpoint para finalizar a compra, alterando o status da compra para FINALIZADO
. No momento que a compra é finalizada, a quantidade de itens em estoque deve ser atualizada, isto é, a quantidade de itens em estoque deve ser reduzida pela quantidade de itens comprados.
- No
views/compra.py
, vamos criar um métodofinalizar
na viewCompraViewSet
:
from django.db import transaction
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
...
class CompraViewSet(ModelViewSet):
...
@action(detail=True, methods=["post"])
def finalizar(self, request, pk=None):
compra = self.get_object()
if compra.status != Compra.StatusCompra.CARRINHO:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={'status': 'Compra já finalizada'},
)
with transaction.atomic():
for item in compra.itens.all():
if item.quantidade > item.livro.quantidade:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
'status': 'Quantidade insuficiente',
'livro': item.livro.titulo,
'quantidade_disponivel': item.livro.quantidade,
},
)
item.livro.quantidade -= item.quantidade
item.livro.save()
compra.status = Compra.StatusCompra.FINALIZADO
compra.save()
return Response(status=status.HTTP_200_OK, data={'status': 'Compra finalizada'})
O decorador
@action
cria um endpoint para a açãofinalizar
, no formatoapi/compras/{id}/finalizar
.
O método
finalizar
é um método de ação que finaliza a compra. Ele recebe a compra que está sendo finalizada.
Se a compra já foi finalizada, retorna um erro.
Se a quantidade de itens em estoque for menor do que a quantidade de itens comprados, retorna um erro.
Se a quantidade de itens em estoque for maior ou igual à quantidade de itens comprados, atualiza a quantidade de itens em estoque e finaliza a compra.
O comando
with transaction.atomic()
garante que todas as operações dentro do blocowith
sejam executadas ou nenhuma seja executada.
O método
save
é chamado para salvar a compra e o livro.
O método
Response
retorna uma resposta HTTP.
O status
HTTP_200_OK
indica que a requisição foi bem sucedida.
O status
HTTP_400_BAD_REQUEST
indica que a requisição não foi bem sucedida.
-
Para testar:
- Tente finalizar uma compra que não foi finalizada.
- Tente finalizar uma compra que já foi finalizada.
- Tente finalizar uma compra com quantidade de itens maior do que a quantidade em estoque.
- Tente finalizar uma compra com quantidade de itens menor ou igual à quantidade em estoque.
-
Faça o commit com a mensagem
feat: finalizando a compra e atualizando a quantidade de itens em estoque
.
Vamos criar uma ação personalizada para gerar um relatório de vendas do mês. Essa ação será aplicada a uma coleção, ou seja, a todas as compras.
- No
views/compra.py
, vamos criar um métodorelatorio_vendas_mes
na viewCompraViewSet
:
@action(detail=False, methods=['get'])
def relatorio_vendas_mes(self, request):
agora = timezone.now()
inicio_mes = agora.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
compras = Compra.objects.filter(status=Compra.StatusCompra.FINALIZADO, data__gte=inicio_mes)
total_vendas = sum(compra.total for compra in compras)
quantidade_vendas = compras.count()
return Response(
{
'status': 'Relatório de vendas deste mês',
'total_vendas': total_vendas,
'quantidade_vendas': quantidade_vendas,
},
status=status.HTTP_200_OK,
)
O decorador
@action
cria um endpoint para a açãorelatorio_vendas_mes
, no formatoapi/compras/relatorio_vendas_mes
.
O método
relatorio_vendas_mes
é um método de ação que gera um relatório de vendas do mês.
O método
timezone.now()
retorna a data e hora atuais.
- Para testar:
- Gere um relatório de vendas do mês.
- Faça o commit com a mensagem
feat: gerando um relatório de vendas do mês
.
Vamos criar uma ação personalizada para listar os livros com mais de 10 cópias vendidas. Essa ação será aplicada a uma coleção, ou seja, a todos os livros.
Para listar os livros que possuem mais de 10 unidades vendidas, você pode usar annotate
para calcular o total de unidades vendidas com base no relacionamento entre os livros e os itens de compra.
A primeira coisa que precisamos fazer é incluir um related_name no campo livro da entidade ItensCompra, em models/compra.py
:
...
class ItensCompra(models.Model):
...
livro = models.ForeignKey(Livro, on_delete=models.PROTECT, related_name='itens_compra')
...
-
Execute as migrações.
-
No
views/livro.py
, vamos criar um métodomais_vendidos
na viewLivroViewSet
:
from django.db.models.aggregates import Sum
...
@action(detail=False, methods=['get'])
def mais_vendidos(self, request):
livros = Livro.objects.annotate(total_vendidos=Sum('itenscompra__quantidade')).filter(total_vendidos__gt=10)
data = [
{
'id': livro.id,
'titulo': livro.titulo,
'total_vendidos': livro.total_vendidos,
}
for livro in livros
]
return Response(data, status=status.HTTP_200_OK)
O decorador
@action
cria um endpoint para a açãomais_vendidos
, no formatoapi/livros/mais_vendidos
.
Utilizamos o método
annotate
comSum
para somar a quantidade de cada livro vendido, usando o relacionamento com a tabelaItensCompra
(itenscompra
sendo orelated_name
definido na model).
A filtragem é feita com
filter(total_vendidos__gt=10)
para incluir apenas livros que tenham mais de 10 unidades vendidas.
Os dados retornados são compostos pelo id e título do livro (livro.id
e livro.titulo
) e pelo total de unidades vendidas (livro.total_vendidos
), obtido pelo annotate
.
Ao fazer uma solicitação GET
para o endpoint /api/livros/mais_vendidos/
, a resposta será algo assim:
[
{
"id": 1,
"titulo": "O Código Limpo",
"total_vendidos": 33
},
{
"id": 2,
"titulo": "O Codificador Limpo",
"total_vendidos": 25
},
]
Nesse momento, é possível apenas listar todos os livros. Vamos ver como podemos filtrar os livros por seus atributos, como categoria
, editora
e autores
.
Para isso, vamos utilizar o pacote django-filter
, que nos permite filtrar os resultados de uma consulta. Ele já está instalado no projeto.
Filtrando os livros por categoria
Vamos começar filtrando os livros por categoria.
- No
views/livro.py
, vamos alterar oviewset
deLivro
para filtrar os livros por categoria:
...
from django_filters.rest_framework import DjangoFilterBackend
...
class LivroViewSet(viewsets.ModelViewSet):
queryset = Livro.objects.all()
serializer_class = LivroSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['categoria__descricao']
...
O
DjangoFilterBackend
é o filtro dodjango-filter
.
O
filterset_fields
indica quais campos serão filtrados. Nesse caso, estamos filtrando apenas pelo campocategoria__descricao
.
- Para testar no
Swagger
, clique no endpointlivros/
e depois emTry it out
. Você verá que apareceu um campocategoria
para filtrar os livros por categoria. Informe adescrição
da categoria e clique emExecute
. Você verá que apenas os livros da categoria informada foram listados. - Para testar no ThunderClient, utilize a url com o seguinte formato:
http://0.0.0.0:19003/api/livros/?categoria__descricao=Python
. Você verá que apenas os livros da categoria informada foram listados.
Acrescentando outros filtros na listagem de livros
Vamos acrescentar outros filtros na listagem de livros.
- No
views/livro.py
, vamos alterar o atributofilterset_fields
, naviewset
deLivro
para filtrar os livros porcategoria__descricao
eeditora__nome
:
...
filterset_fields = ['categoria__descricao', 'editora__nome'] # Acrescentando o filtro por editora
...
O
filterset_fields
indica quais campos serão filtrados. Nesse caso, estamos filtrando pelos camposcategoria__descricao
eeditora__nome
.
Da mesma forma, por outros campos.
- Para filtrar por categoria e editora:
- Para filtrar apenas por editora:
- Para filtrar apenas por categoria:
Exercício
-
Acrescente filtros nas models
Autor
,Categoria
,Editora
,Livro
eCompra
. -
Faça o commit com a mensagem
feat: utilizando filtros
.
A busca textual serve para adicionar a funcionalidade de realizar buscas dentro de determinados valores de texto armazenados na base de dados.
Contudo a busca só funciona para campos de texto, como CharField
e TextField
.
-
Para utilizar a busca textual nos livros, devemos promover duas alterações em nossa
viewset
: -
Novamente alterar o atributo
filter_backends
, adicionando o BackendSearchFilter
que irá processar a busca; e -
Adicionar o atributo
search_fields
, contendo os campos que permitirão a busca. -
A
LivroViewSet
ficará assim:
...
from rest_framework.filters import SearchFilter
...
class LivroViewSet(viewsets.ModelViewSet):
...
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['categoria__descricao', 'editora__nome']
search_fields = ['titulo']
...
- Para pesquisar por um livro, basta adicionar o parâmetro
search
na URL, com o valor a ser pesquisado. Por exemplo, para pesquisar por livros que contenham a palavrapython
no título, a URL ficaria assim:
Exercício
-
Acrescente a busca textual nas models
Autor
,Categoria
,Editora
eCompra
. -
Faça o commit com a mensagem
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.
- Para utilizar a ordenação nos livros, devemos promover três alterações em nossa
ViewSet
: - Novamente alterar o atributo
filter_backends
, adicionando o BackendOrderingFilter
que irá processar a ordenação; e - Adicionar o atributo
ordering_fields
, contendo os campos que permitirão a ordenação. - Adicionar o atributo
ordering
com o campo que será utilizado como padrão para ordenação. - A
LivroViewSet
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']
...
- Para ordenar os livros, basta adicionar o parâmetro
ordering
na URL, com o valor do campo a ser ordenado. - Se você não coloca o parâmetro
ordering
, a ordenação será feita pelo campo definido no atributoordering
, nesse caso,titulo
: - Compare com este exemplo, e perceba que a saída é a mesma:
- Para mostrar na ordem reversa, basta adicionar um sinal de menos (-) na frente do campo:
- Ou mostrar os livros ordenando pelo preço:
- Pode-se ainda juntar a ordenação com a busca textual. Por exemplo, para ordenar os livros pelo título e que contenham a palavra
python
no título, a URL ficaria assim: - Para utilizar os filtros e a ordenação, basta adicionar os parâmetros na URL, com os valores desejados. Por exemplo, para ordenar os livros pelo título de uma determinada categoria e editora, a URL ficaria assim:
- É possível utilizar todos os recursos ao mesmo tempo: múltiplos filtros, busca textual e ordenação.
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.
- No
views/compra.py
, vamos alterar o atributofilterset_fields
, naviewset
deCompra
para filtrar as compras pordata
. - Vamos também alterar o atributo
ordering_fields
, naviewset
deCompra
para ordenar as compras pordata
.
...
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_fields = ['usuario__email', 'status', 'data']
search_fields = ['usuario__email']
ordering_fields = ['usuario__email', 'status', 'data']
ordering = ['-data']
...
- Para ordenar por data, em ordem descrente:
Exercício
- Acrescente a ordenação nas models
Autor
,Categoria
,Editora
eCompra
. - Faça o commit com a mensagem
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.
- No
serializers/compra.py
, vamos alterar o serializer chamadoCompraCreateUpdateSerializer
:
class CompraCreateUpdateSerializer(ModelSerializer):
usuario = HiddenField(default=CurrentUserDefault())
itens = ItensCompraCreateUpdateSerializer(many=True)
class Meta:
model = Compra
fields = ('usuario', 'itens')
def create(self, validated_data):
itens = validated_data.pop('itens')
usuario = validated_data['usuario']
compra, criada = Compra.objects.get_or_create(
usuario=usuario, status=Compra.StatusCompra.CARRINHO, defaults=validated_data
)
for item in itens:
item_existente = compra.itens.filter(livro=item['livro']).first()
if item_existente:
item_existente.quantidade += item['quantidade']
item_existente.preco = item['livro'].preco
item_existente.save()
else:
item['preco'] = item['livro'].preco
ItensCompra.objects.create(compra=compra, **item)
return compra
def update(self, compra, validated_data):
itens = validated_data.pop('itens', [])
if itens:
compra.itens.all().delete()
for item in itens:
item['preco'] = item['livro'].preco
ItensCompra.objects.create(compra=compra, **item)
return super().update(compra, validated_data)
O método
get_or_create
retorna um objetoCompra
existente ou cria um novo objetoCompra
se ele não existir.
O método
filter
retorna um objetoItensCompra
que atenda aos critérios de pesquisa.
O método
first
retorna o primeiro objetoItensCompra
que atenda aos critérios de pesquisa ouNone
se não houver objetos.
Adicionar um campo total
ao modelo de Compra
para armazenar o valor total é uma solução eficaz em termos de performance e facilidade de uso em consultas frequentes. Com isso, o valor total será calculado e armazenado diretamente no banco de dados, permitindo que você ordene ou filtre pelas compras com eficiência.
- No
models/compra.py
, vamos adicionar o campototal
ao modeloCompra
:
class Compra(models.Model):
...
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
...
Vamos também incluir um método save
para calcular o total da compra:
class Compra(models.Model):
...
def save(self, *args, **kwargs):
self.total = sum(item.preco * item.quantidade for item in self.itens.all())
super().save(*args, **kwargs)
...
O método
save
é um método especial que é chamado sempre que um objeto é salvo no banco de dados.
O método
super().save(*args, **kwargs)
chama o métodosave
da classe pai.
O método
sum
retorna a soma de todos os valores em um iterável.
O método
self.itens.all()
retorna todos os itens da compra.
Podemos retirar a property total
da classe Compra
:
class Compra(models.Model):
...
@property
def total(self):
return sum(item.preco * item.quantidade for item in self.itens.all())
...
Precisamos ainda incluir o salvamento da compra no método create
do serializer CompraCreateUpdateSerializer
:
class CompraCreateUpdateSerializer(ModelSerializer):
...
def create(self, validated_data):
...
compra.save() # linha adicionada para salvar a compra
return compra
...
O método
save
é chamado para salvar a compr, atualizando assim o campototal
.
-
Execute as migrações.
-
Atualize os valores do campo
total
para as compras existentes, utilizando o shell do Django:
for compra in Compra.objects.all():
compra.save()
- Para testar se o campo
total
está sendo preenchido corretamente:- crie uma nova compra
- inclua um novo livro no carrinho
- altera a quantidade do livro
Ordenações e consultas
Após adicionar o campo total, você pode usá-lo diretamente no shell do Django em consultas para ordenar ou filtrar as compras.
- Ordenar as compras pelo total, em ordem decrescente:
compras = Compra.objects.all().order_by('-total')
- Filtrar as compras pelo total, com um valor mínimo de 100:
compras = Compra.objects.filter(total__gte=100)
- Faça o commit com a mensagem
feat: adicionando o total da compra
.
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.
- Pode chamar o repositório de
garagem
. - Nomeie o commit como sendo
feat: Criação do projeto
. - Siga esses passos para criar a API.
- Você pode utilizar o script de criação da API também, conforme explicado aqui.
- Crie as seguintes APIs, fazendo um commit para cada etapa:
Acessorio
:descricao
(string, máximo 100 caracteres).__str__
(retorna o id e a a descrição).- Exemplos:
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).- Exemplo:
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).- Exemplo:
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
.
- Crie a API para o
Veiculo
no projetoGaragem
.- Crie o modelo
Veiculo
, com os seguintes atributos:ano
(inteiro, permite nulo, default 0).preco
(decimal, máximo 10 dígitos, 2 casas decimais, permite nulo, default 0).modelo
(chave estrangeira paraModelo
).cor
(chave estrangeira paraCor
).acessorios
(chave estrangeira paraAcessorio
, muitos para muitos).__str__
(retorna o id, modelo, cor e ano do carro).
- Crie a API REST para o modelo
Veiculo
.
- Crie o modelo
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:
- Clique no ícone de engrenagem no canto inferior esquerdo da tela do VS Code e clique em
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:
- DotENV (Suporte a arquivos
.env
) - ESLint (JavaScript)
- Even Better TOML (Melhorias na edição de arquivos TOML)
- Intellicode (Desenvolvimento Inteligente)
- Markdown All in One (Edição de arquivos Markdown)
- Peacock (Personalização de cores)
- Portuguese (Brazil) Language Pack for Visual Studio Code (Tradução para Português da interface do VS Code)
- Prettier (Formatação de código)
- Python (Uhuuuu!)
- Ruff (Linter e formatador de código)
- SqLite Viewer (Visualização de bancos de dados SQLite)
- Thunder Client (Teste de APIs)
- TODO Highlight (Realce de TODOs)
- vscode-icons (Ícones para o VS Code)
- Vue - Official (Desenvolvimento de aplicações Vue.js)
- Vue 3 Support - All in One (Suporte ao Vue 3)
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:
- Faça login com a conta do GitHub ou da Microsoft no VS Code.
- Clique no ícone de engrenagem no canto inferior esquerdo da tela do VS Code e clique em
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
- Se não estiver instalado, instale a versão mais recente:
curl -sSLv https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -
- Após a instalação, feche o terminal (
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)
- Execute o seguinte comando:
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)
- Execute o seguinte comando:
curl -sSL https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/pdm_config_ohmyzsh.sh | zsh
Instalação do PDM no Windows
Execute o comando abaixo no PowerShell (pode ser no Terminal do VS Code
):
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py -UseBasicParsing).Content | python -
Verifique se o PDM está configurado para não usar virtualenv:
pdm config
IMPORTANTE: Se você não fizer essa configuração, o PDM irá criar uma pasta
.venv
no diretório do projeto. Para resolver isso, você deve apagar a pasta.venv
e executar o comandopdm config python.use_venv false
e 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:
- Acesse o site do Supabase.
- Crie uma conta ou conecte-se no Supabase.
- Clique na opção Start your project.
- Dẽ um nome ao projeto.
- Selecione a opção
Create a new organization
. - Dẽ um nome à organização.
- Dê um nome ao banco de dados.
- Escolha uma senha uma clique ana oção de gerar uma senha e guarde-a (você vai precisar dela).
- Selecione a região
South America (São Paulo)
.
Configurando o banco de dados no projeto
- Entre no Dashboard do projeto, e escolha o projeto criado.
- Clique na opção
Connect
(Conectar), ao lado do nome do projeto. - Copia a linha de conexão do banco de dados da opção
Session Pooler
.- Ela deve ser parecida com isso:
postgresql://postgres.kfjxquvsjldesrrjqgzo:[YOUR-PASSWORD]@aws-0-sa-east-1.pooler.supabase.com:5432/postgres
- Ela deve ser parecida com isso:
- Coloque a senha que você gerou no campo
[YOUR-PASSWORD]
. - Copie a linha de conexão e cole no arquivo
.env
do projeto, como no exemplo:
# Supabase
DATABASE_URL=postgresql://postgres.kfjxquvsjldesrrjqgzo:[email protected]:5432/postgres
Migrando o banco de dados
- No arquivo
.env
:- Descomente a linha
DATABASE_URL
.
- Descomente a linha
- Faça a migracão do banco de dados:
pdm run migrate
Observe que o banco de dados foi migrado no
Supabase
.
Para testar, crie alguns registros no banco de dados. Depois volte a configuração local e perceba que os dados são diferentes na base local e na base do Supabase.
- No site do
Supabase
, acesse oTable Editor
e verifique que as tabelas foram criadas. - Você também pode ver o esquema das tabelas, em
Database
,Schema Visualizer
.
Carregando os dados iniciais
- Para carregar os dados iniciais no banco de dados do Supabase, acesse a aula sobre dump e load de dados.
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:
- Para voltar a usar o banco de dados local, no arquivo
.env
:- Comente a linha
DATABASE_URL
.
- Comente a linha
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
).
- Verifique se seu projeto já possui o arquivo
build.sh
na raiz do projeto.
Testando a execução localmente
- Execute a seguinte linha de comando para testar a execução localmente:
pdm run python -m gunicorn app.asgi:application -k uvicorn.workers.UvicornWorker
- Acesse o endereço
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
Uvicorn
eGunicorn
.
Configurando o Render
-
Acesse o site do Render
-
Crie uma conta ou conecte-se a uma conta existente.
-
Crie um novo serviço (Web Service).
-
Escolha a opção
Build and deploy from a Git repository
(Construir e implantar a partir de um repositório Git). -
Escolha o repositório do projeto.
-
Preencha as informações necessárias:
- Name:
nome-do-projeto
. - Region:
Ohio (US East)
. - Branch:
main
. - Runtime:
Python
. - Build command:
./build.sh
. - Start command:
python -m gunicorn app.asgi:application -k uvicorn.workers.UvicornWorker
. - Instance Type:
Free
.
- Name:
-
Environment Variables: clique em
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_KEY
nova. Veja como aqui. Coloque essa chave no lugar de[sua_secret_key]
.
Coloque a URL do banco de dados do Supabase no lugar de
[sua_database_url]
.
- Clique em
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
- Acesse o site do Cloudinary e crie uma conta.
Configurando o Cloudinary
- Edite o arquivo
.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
.
- Inclua essa mesma variável no
Render
(ou no serviço de hospedagem que você estiver utilizando), na opçãoEnvironment variables
.
Testando
- Coloque a variável
MODE
com o valorMIGRATE
no arquivo.env
. - Faça o upload de uma imagem pelo
Admin
doDjango
e verifique se ela foi salva noCloudinary
, na opçãoMedia Explorer
. - Se deu certo, sua aplicação deve estar funcionando normalmente, utilizando o
Cloudinary
para armazenar os arquivos estáticos. - Faça o commit com a mensagem
feat: adicionando Cloudinary
.
- Ao tentar executar o comando:
pdm run python manage.py runserver
- Se você receber o seguinte erro:
Error: That port is already in use.
- Execute o seguinte comando:
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
- Se seu projeto tiver a pasta
.venv
, e não a pasta__pypackages__
, remova a pasta.venv
:
rm -rf .venv
- Depois, execute novamente o script de configuração do pdm, da aula 1.
- Opcionalmente, rode o seguinte comando, para configurar o projeto para não usar ambiente virtual:
pdm config python.use_venv false
- Feito isso, execute o
pdm install
novamente. - Por fim, execute o
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.
- Para gerar uma nova SECRET_KEY (chave secreta), a ser colocada no arquivo
.env
, execute o comando:
python -c "import secrets; print(secrets.token_urlsafe())"
- No Django, o comando é:
pdm run python manage.py shell -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
- Você também pode gerar uma nova chave secreta em https://djecrety.ir/
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.
- Acesse o site https://sqliteviewer.app/ e abra o arquivo
db.sqlite3
do projeto.
- Adicione as seguintes linhas ao arquivo
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:
- Antes de clicar ou responder, leia atentamente as instruções.
- Leia atentamente as instruções antes de clicar ou responder.
- Nunca clique ou responda sem antes ler atentamente as instruções.
As 3 regras falam a mesma coisa? Sim, você entendeu o recado. ;-)
Configurando o projeto git
- Se o computador estiver configurado com contas individuais, você precisará fazer isso apenas uma vez. Ainda assim, é bom verificar se está tudo certo.
- Verifique se já não existe uma conta conectada ao GitHub no VS Code, clicando no ícone Contas na barra lateral esquerda. Deve ser o penúltimo ícone da baixo pra cima. Se houver, desconecte primeiro.
- Inicialize o repositório git. Clique no ícone do git no painel lateral esquerdo. Deve ser o segundo ícone, de cima pra baixo. Opcionalmente, tecle (
Control+Shift+G
). Depois, clique no botãoInitialize repository
. - Se aparecer uma bolinha azul no ícone do git com um número, o repositório foi ativado. Esse número indica o número de arquivos que foram criados ou alterados.
- Se aparecem muitos arquivos alterados (10 mil, por exemplo), é provável que exista um repositório git criado na pasta raiz do usuário. Apague este repositório assim:
rm -Rf ~/.git
- Recarregue a janela do VS Code:
Control + Shift + P + "Recarregar a Janela"
- Verifique se o número mudou para algo mais razoável (em torno de 100 arquivos).
Configurando as variáveis do git
- Informe seu nome e e-mail no git. Para isso, abra o terminal do VS Code e digite:
git config --global user.name "Seu Nome"
git config --global user.email "[email protected]"
- Para verificar se as variáveis foram configuradas corretamente, digite:
git config -l
- Se aparecer outro nome de usuário ou outras informações estranhas, remova o arquivo com as configurações globais do git:
rm ~/.gitconfig
Repita o processo de configuração de nome e e-mail.
- Liste todas as categorias:
curl -X GET http://0.0.0.0:19003/api/categorias/
- Liste uma categoria específica:
curl -X GET http://0.0.0.0:19003/api/categorias/1/
- Crie uma nova categoria:
curl -X POST http://0.0.0.0:19003/api/categorias/ -d "descricao=Teste"
- Atualize uma categoria:
curl -X PUT http://0.0.0.0:19003/api/categorias/1/ -d "descricao=Teste 2"
- Delete uma categoria:
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()
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:
- Remover todos os registros de uma tabela:
DELETE FROM core_categoria;
- Remover todos os usuários, com exceção do primeiro usuário cadastrado:
DELETE FROM core_user WHERE id > 1;
- Atualizar o preço de todos os livros com preço nulo para 10:
UPDATE core_livro SET preco = 10 WHERE preco IS NULL;
- Atualizar o preço de todos os livros com preço igual a zero para 10:
UPDATE core_livro SET preco = 10 WHERE preco = 0;
- Listar todos os livros com preço igual a zero:
SELECT * FROM core_livro WHERE preco = 0;
- Listar todos os livros com preço nulo:
SELECT * FROM core_livro WHERE preco IS NULL;
- Listar todos os livros de uma categoria específica:
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 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.
Para contriburi com este projeto:
- Criar um fork do projeto.
- Clonar o fork
- Criar um branch para a sua contribuição.
- Fazer as alterações no seu branch.
- Enviar um pull request para o projeto original.
Marco André Mendes <[email protected]>