Tutorial para desenvolvimento de APIs REST usando o Django com DRF (Django Rest Framework). Esse tutorial foi construído a partir do curso em vídeo Django com DRF do Eduardo da Silva.
Este tutorial está em constante desenvolvimento. Envie sugestões e correções para meu email. Se preferir, faça uma solicitação de contribuição ao projeto.
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 usando JavaScript
- Desenvolvimento Web II (Prof. Eduardo da Silva)
- Desenvolvimento front-end com VueJS
- Desenvolvimento Web III (Prof. Marco André Lopes Mendes)
- Desenvolvimento back-end com Django e DRF.
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:
a@a.com
- 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
(tabelas) da aplicação.serializers
: é a pasta onde ficam os serializadores da aplicação.views
: é a pasta onde ficam as views 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 poque 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 dos 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
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. - Escreva mensagens como:
- Modificação do arquivo
models.py
- Inclusão da Categoria de Veículos
- Alteração do Marca do Veículo
- Modificação do arquivo
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
Um serializer é um objeto que transforma um objeto do banco de dados em um objeto JSON.
- 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 oserializer
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: as nomes das rotas serão sempre nomes únicos, no plural e em minúsculas. Na 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
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/livraria-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 na pastamodels
. - Incluir a
model
no__init__.py
da pastamodels
. - Incluir a
model
noAdmin
. - Fazer a migração e efetivar a migração.
- Criar o serializador na pasta
serializers
. - Incuir o serializador no
__init__.py
da pastaserializers
. - Criar a
viewset
na pastaviews
. - Incluir a
viewset
no__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 Categoria, Editora
...
class EditoraSerializer(ModelSerializer):
class Meta:
model = Editora
fields = "__all__"
serializers/__init__.py
...
from .editora import EditoraSerializer
views/editora.py
from rest_framework.viewsets import ModelViewSet
from core.models import Categoria, Editora
from core.serializers import CategoriaSerializer, EditoraSerializer
...
class EditoraViewSet(ModelViewSet):
queryset = Editora.objects.all()
serializer_class = EditoraSerializer
views/__init__.py
...
from .editora import EditoraViewSet
urls.py
...
from core.views import UserViewSet, CategoriaViewSet, EditoraViewSet
...
router.register(r"categorias", CategoriaViewSet)
router.register(r"editoras", EditoraViewSet)
...
6.3 Fazendo a migração e efetivando a migração
- 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
- 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
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 email de no máximo 100 caracteres, que pode ser nulo. -
Teste a API.
-
Faça o commit, com a mensagem
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 dos arquivos necessários
Utilizando um comando no terminal, é possível criar todos os arquivos necessários para a criação da API para a classe Livro
.
touch core/models/livro.py core/serializers/livro.py core/views/livro.py
É possivel também abrir todos os arquivos de uma vez, utilizando o comando:
code core/models/livro.py core/models/__init__.py core/serializers/livro.py core/serializers/__init__.py core/views/livro.py core/views/__init__.py app/urls.py core/admin.py
8.2 Criando o modelo de dados Livro
- Vamos criar o modelo de dados
Livro
, no arquivomodels.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.titulo} ({self.quantidade})"
Antes de efetivarmos as alterações no banco de dados, vamos incluir duas chaves estrangeiras no modelo Livro
.
8.3 Incluindo chaves estrangeiras no modelo
Nosso livro terá uma categoria e uma editora. Para isso, vamos incluir campos que serão chaves estrageiras, referenciando os modelos Categoria
e Editora
.
8.3.1 Campo categoria
no Livro
- Inclua a linha a seguir no modelo
Livro
, logo após o atributopreco
:
...
categoria = models.ForeignKey(
Categoria, on_delete=models.PROTECT, related_name="livros"
)
...
- Vamos entender cada parte:
models.ForeignKey
: define o campo como sendo uma chave estrangeira.Categoria
: o model que será associado a esse campo.on_delete=models.PROTECT
: impede de apagar uma categoria que possua livros associados.related_name="livros"
: cria um atributolivros
na classeCategoria
, permitindo acessar todos os livros de uma categoria.
8.3.2 Campo editora
no Livro
- De forma semelhante, vamos associar o livro a uma editora, incluindo logo em seguida à categoria, a seguinte linha:
editora = models.ForeignKey(Editora, on_delete=models.PROTECT, related_name="livros")
8.4 Inclusão dos modelos no Admin
- Inclua os modelos criados no arquivo
admin.py
:
from django.contrib import admin
from livraria.models import Autor, Categoria, Editora, Livro
admin.site.register(Autor)
admin.site.register(Categoria)
admin.site.register(Editora)
admin.site.register(Livro)
8.5 Efetivando as alterações no banco de dados
- Prepare as migrações:
pdm run python manage.py makemigrations
- Efetive as migrações:
pdm run python manage.py migrate
8.6 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?
8.7 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 python manage.py shell
- Acesse os livros da categoria com
id
1:
>>> from livraria.models import Categoria
>>> Categoria.objects.get(id=1).livros.all()
8.3 Criação da API para Autor e Livro
8.3.1 Criação da API para Autor
- Crie a API para a classe
Autor
seguindo os passos anteriores. - Teste o funcionamento.
- Faça o commit.
8.3.2 Criação da API para Livro
- Crie a API para a classe
Livro
seguindo os passos anteriores. - Teste o funcionamento.
- Observou que no
Livro
, aparecem apenas os camposid
da categoria e da editora, e não o nome?
8.4 Criação de múltiplos serializadores
8.4.1 Apresentação das informações de categoria e editora 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 no criação e alteração (POST, PUT e PATCH).
Para resolver isso, podemos criar dois (ou mais) serializadores, como no exemplo:
class LivroSerializer(ModelSerializer):
class Meta:
model = Livro
fields = "__all__"
class LivroDetailSerializer(ModelSerializer):
class Meta:
model = Livro
fields = "__all__"
depth = 1
Na viewset, escolhemos o serializador conforme a operação:
class LivroViewSet(ModelViewSet):
queryset = Livro.objects.all()
def get_serializer_class(self):
if self.action in ["list", "retrieve"]:
return LivroDetailSerializer
return LivroSerializer
8.4.2 Criação de um serializador para a listagem de livros
- Crie um serializador para a listagem de livros, que mostre apenas o id, o título e o preço.
class LivroListSerializer(ModelSerializer):
class Meta:
model = Livro
fields = ["id", "titulo", "preco"]
- Altere a viewset para utilizar esse serializador na listagem:
def get_serializer_class(self):
if self.action == "list":
return LivroListSerializer
elif self.action == "retrieve":
return LivroDetailSerializer
return LivroSerializer
-
Teste a API. Observe que a listagem de vários livros está diferente da recuperação de um único livro.
-
Teste a API.
8.5 Exercício: Crie a API REST no projeto Garagem para as demais classes
Model com ManyToManyField - Livros com vários autores
Um livro pode ter vários autores, por isso criaremos agora um relacionamento n para n entre Livro
e Autor
. Para isso utilizaremos um campo do tipo ManyToManyField
.
- Inclua o campo
autores
no modeloLivro
:
...
autores = models.ManyToManyField(Autor, related_name="livros")
...
- Crie as migrações:
pdm run python manage.py makemigrations
- Execute as migrações:
pdm run python manage.py migrate
Feito isso, observe no banco de dados que esse campo não foi criado na tabela de livros. Ao invés disso, uma tabela associativa foi criada, com o nome livraria_livro_autores
, contendo os campos livro_id
e autor_id
. É assim que é feito um relacionamento n para n no Django.
10.1 Exercícios
10.1.1 No projeto Livraria
10.1.1.1 No Admin:
- Entre no Admin;
- Cadastre alguns autores;
- Cadastre alguns livros com mais do que um autor.
10.1.1.2 Na API:
- Teste a API REST de livros e autores.
10.2 Exercício: Crie um relacionamento n para n entre Veiculo
e Acessorio
no projeto Garagem
11.1 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 esse 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
Ação | HTTP | CRUD | Admin |
---|---|---|---|
Criar | POST | Create | add |
Ler | GET | Read | view |
Atualizar | PUT (PATCH) | Update | change |
Deletar | DELETE | Delete | delete |
11.2 Criando grupos e dando permissões
Vamos começar criando 2 grupos e dando a eles permissões distintas:
- Crie um grupo chamado
compradores
, com as seguintes permissões:- Visualizar:
autor
,categoria
eeditora
. - Adicionar, editar e visualizar:
livro
.
- Visualizar:
- Crie um grupo chamado
administradores
, com as seguintes as permissões:- Adicionar, editar, visualizar e remover:
autor
,categoria
,editora
elivro
.
- Adicionar, editar, visualizar e remover:
11.3 Criando usuários e adicionando aos grupos
- Crie um usuário
admin1
e o inclua no grupoadministradores
. - Crie um usuário
comprador1
e o inclua no grupocompradores
.
12.1 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á, normamente 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 ou viewsets, por exemplo) ou de forma global, no arquivo settings.py
.
12.2 Exemplo de uso de permisssão na viewset
Como ilustração, modifique o arquivo views.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 um erro.
- Agora entre novamente pelo Admin.
- Tente acessar as categorias pelo DRF.
- Você deve conseguir.
12.3 Exemplo de uso de permisssão no settings.py
IMPORTANTE: Outra forma de gerenciamento de permissões é feita no arquivo
settings.py
. Para utilizá-la, comente as últimas alterações feitas no arquivoviews.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",
]
}
Inclua o código acima e teste novamente o acesso aos endpoints do DRF (categorias, editoras, etc.) com e sem uma sessão autenticada.
12.4 Permissões com o DjangoModelPermissions
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.
Para isso, teremos que alterar a classe de autenticação, substituindo o que colocamos anteriormente:
REST_FRAMEWORK = {
...
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.DjangoModelPermissions",
],
...
}
Resumindo, utilizaremos a estrutura de usuários, grupos e permissões que o próprio Django já nos fornece. Para isso, utilizaremos o
DjangoModelPermissions
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 SimpleJWT.
Um resumo sobre autenticação e autorização
Relembrando o que estudamos até aqui em termos de autenticação e autorização:
- Como criar grupos e usuários e inserir os usuários nesses grupos
- Como dar permissões nas models (via Admin) para visualização (
view
), adição (add
), alteração (change
) e exclusão (remove
). - Como utilizar diversas formas de gerenciamento de permissões no Django, incluindo as permissões em cada
view
ou as permissões padrão nosettings.py
. - Como utilizar o
DjangoModelPermissions
para fazer uso do gerenciamento de permissões já incluído no Django Admin.
Agora, vamos utilizar o SimpleJWT para a autenticação no Django REST Framework.
Resumindo, utilizaremos o SimpleJWT para autenticação e a estrutura de permissões do Django para autorização.
O SimpleJWT
O SimpleJWT é um plug-in de autenticação JSON Web Token para o Django REST Framework.
Instalação e configuração
- Para instalar o SimpleJWT, execute o seguinte comando:
pdm add djangorestframework-simplejwt
- Adicione o
SimpleJWT
no arquivosettings.py
:
INSTALLED_APPS = [
...
"rest_framework_simplejwt",
...
]
- Adicione o
SimpleJWT
no arquivosettings.py
:
REST_FRAMEWORK = {
...
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
...
}
- Adicione o
SimpleJWT
no arquivourls.py
:
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
...
]
- Feitas essa aterações, coloque o servidor do Django novamente em execução.
Exercícios: Testando as permissões com o SimpleJWT
Para testar se tudo deu certo, utilizaremos um cliente HTTP, como o Thunder Client.
Colocando as informações do token na requisição
Dica: se sua ferramenta permitir, crie várias requisições separadas e dê nomes, como login, consulta, inclusão, etc.
- Ao tentar acessar um endpoint com
GET
, como esse:
[GET] http://0.0.0.0:19003/api/categorias/
- Você deverá receber uma resposta parecida com essa:
{
"detail": "As credenciais de autenticação não foram fornecidas."
}
- Para fazer a autenticação, precisamos enviar as informações de
usuário
esenha
. Faremos isso enviando uma requisição do tipoPOST
, com as seguintes informações, noBody
emJSON
:
{
"username": "comprador1",
"password": "senha.123"
}
-
O endereço para envio da requisição é o seguinte:
IMPORTANTE: Não esqueça da barra (
/
) final no endereço e lembre-se que essa é uma requisição do tipoPOST
.
Você deve receber uma resposta semelhante a essa:
{
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY2MTcyNDUxMCwiaWF0IjoxNjYxNjM4MTEwLCJqdGkiOiJiN2RhNWZkMjEwYTI0NjliOWE0MjgxZjQxZDcwNjZhMCIsInVzZXJfaWQiOjN9.lATd6io76oVa6nW5zuBEtsa8htvsL6wVhp-KzXMK-rk",
"access": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjYxNjM4NDEwLCJpYXQiOjE2NjE2MzgxMTAsImp0aSI6ImRhYTBmNDcyZDI3YTQ5ZTM4M2I4ZjJhOTcwYjhlMWNmIiwidXNlcl9pZCI6M30.HY2j0L6eQBaPxAoHrPz_KFK_sWyb9lHmR7dQ1sOPTNY"
}
Para verificar as informações do token, acesse os sites jwt.io ou token.dev, cole o valor do token no campo
Encoded
e clique no botãoDecode
.
Todas as chamadas ao sistema que precisarem de autenticação deverão ser feitas com o campo access
token. Para isso, copie o valor do campo access
e cole no campo Auth
, opção Bearer
do Thunder Client.
Para testar, acesse com GET
o seguinte endereço:
[GET] http://0.0.0.0:19003/api/categorias/
Você deverá conseguir visualizar todas as categorias cadastradas.
Token expirado
Quando o token expira, você receberá uma resposta semelhante a essa:
{
"detail": "O token informado não é válido para qualquer tipo de token",
"code": "token_not_valid",
"messages": [
{
"token_class": "AccessToken",
"token_type": "access",
"message": "O token é inválido ou expirado"
}
]
}
Para renovar o token, faça novamente a requisição de autenticação, enviando as informações de usuário e senha.
Tentando alterar uma informação
[PUT] http://0.0.0.0:19003/api/categorias/10/
{
"descricao": "Cobol"
}
{
"detail": "Método \"PUT\" não é permitido."
}
Você não pode alterar uma informação com esse usuário. Para isso, você precisa de um usuário com permissão de escrita.
Testando com outro usuário
Repita o processo de autenticação e consulta com o usuário admin1
que criamos anteriormente.
Resumindo, você vai precisar:
- Criar uma requisição de autenticação, do tipo
POST
, para a URLtoken
, enviando as informações de usuário e senha. - Copiar a chave do tipo
access
e colocá-la no cabeçalhoAuth
, opçãoBearer
da requisição do tipoGET
que você fará.
Com isso, fizemos um sistema básico de autenticação (login) e autorização (permissões) usando o próprio sistema já fornecido pelo Django.
Vamos instalar uma aplicação para gerenciar o upload de imagens e sua associação ao nosso modelos.
Configuração Baixe e descompacte o arquivo com a app pronta para ser utilizada:
wget https://github.com/marrcandre/django-drf-tutorial/raw/main/apps/uploader.zip -O uploader.zip && unzip uploader.zip && rm uploader.zip
No Windows
, execute os seguintes comandos no PowerShell
:
Invoke-WebRequest -Uri https://github.com/marrcandre/django-drf-tutorial/raw/main/apps/uploader.zip -OutFile uploader.zip
Expand-Archive -Path uploader.zip -DestinationPath .
Remove-Item -Force uploader.zip
O projeto ficará com uma estrutura parecida com essa:
.
├── livraria
├── config
├── uploader
│ ├── models
│ │ ├── document.py
│ │ ├── image.py
│ │ └── __init__.py
│ ├── router.py
│ ├── serializers
│ │ ├── document.py
│ │ ├── image.py
│ │ └── __init__.py
├── static
└── utils
└── files.py
- Instalar os pacotes
python-magic
ePillow
:
pdm add python-magic Pillow
Configuração no Windows
-
Caso você esteja no Windows, para funcionar corretamente, você precisará seguir os seguintes passos:
-
Baixe as DLLs: Acesse https://github.com/pidydx/libmagicwin64 e baixe o arquivo ZIP contendo as DLLs.
-
Descompacte o arquivo ZIP: Clique com o botão direito no arquivo ZIP baixado e escolha "Extrair tudo..." para extrair o conteúdo.
-
Copie as DLLs: Dentro da pasta extraída, você encontrará os arquivos DLL (
magic1.dll
, etc.). Selecione esses arquivos, clique com o botão direito e escolha "Copiar." -
Cole as DLLs no System32: Navegue até o diretório
C:\Windows\System32
, crie uma pasta chamadamagic
, clique com o botão direito dentro da pasta e escolha "Colar." Você pode precisar de permissões administrativas para fazer isso, então certifique-se de confirmar qualquer solicitação que apareça. -
Acrescentando o caminho da pasta:
-
Abra o menu Iniciar em seu computador.
-
Utilize a barra de pesquisa para procurar por "Editar as variáveis de ambiente do sistema".
-
Clique no resultado que corresponde a essa pesquisa para abrir a janela de propriedades do sistema.
-
Na janela de propriedades do sistema, vá até a aba chamada "Avançado".
-
Dentro dessa aba, localize o botão com o rótulo "Variáveis de Ambiente" e clique nele. Isso abrirá a janela de Configurações das Variáveis de Ambiente.
-
Na janela que se abre, procure pela opção chamada "Path" na seção de variáveis do sistema e clique sobre o botão "Editar".
-
Verifique se o caminho completo para a pasta "magic" está listado na lista de caminhos. Caso não esteja, prossiga com a próxima etapa.
-
Abra o Explorador de Arquivos e encontre a pasta "magic".
-
Copie o caminho completo da pasta "magic".
-
Volte para a janela de "Editar as variáveis de ambiente". Cole o caminho copiado da pasta "magic" no campo apropriado dentro da janela.
-
Confirme as alterações clicando em "OK".
-
-
Atualize seu código: No arquivo
utils/files.py
, onde você está usando a bibliotecamagic
, você precisará especificar o caminho para o arquivomagic.mgc
. Aqui está um exemplo de como você pode fazer isso:import magic file_magic = magic.Magic(magic_file="C:\Windows\magic\magic.mgc")
Seguindo esses passos, você deverá conseguir importar a biblioteca
magic
com sucesso em seu projeto Django no ambiente Windows. -
Registro da app
- Adicione o pacote
uploader
na lista deINSTALLED_APPS
, nosettings.py
:
INSTALLED_APPS = [
...
"uploader",
"livraria",
...
]
Configuração no settings.py
- Ainda no
settings.py
faça as seguintes configurações:
# App Uploader settings
MEDIA_URL = "http://0.0.0.0:19003/api/media/"
MEDIA_ENDPOINT = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media/")
FILE_UPLOAD_PERMISSIONS = 0o640
- Você vai precisar incluir o módulo
os
no início do arquivosettings.py
:
import os
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
...
path("api/media/", include(uploader_router.urls)),
...
urlpatterns += static(settings.MEDIA_ENDPOINT, document_root=settings.MEDIA_ROOT)
...
Migração do banco de dados
- Faça a migração do banco de dados:
pdm run python manage.py makemigrations uploader
pdm run python manage.py migrate
Uso em modelos
- 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.CASCADE,
null=True,
blank=True,
default=None,
)
- Faça novamente a migração do banco de dados:
pdm run python manage.py makemigrations livraria
pdm run python manage.py migrate
Uso no serializer
- Edite o arquivo
serializers/livro.py
da aplicaçãolivraria
e inclua o seguinte conteúdo:
...
from rest_framework.serializers import ModelSerializer, SlugRelatedField
from uploader.models import Image
from uploader.serializers import ImageSerializer
...
class LivroSerializer(ModelSerializer):
capa_attachment_key = SlugRelatedField(
source="capa",
queryset=Image.objects.all(),
slug_field="attachment_key",
required=False,
write_only=True,
)
capa = ImageSerializer(required=False, read_only=True)
...
class LivroDetailSerializer(ModelSerializer):
...
capa = ImageSerializer(required=False)
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/api/media/images/
e observe que a imagem foi associada ao livro.
Vamos instalar uma aplicação para gerar a documentação da API usando o Swagger e o Redoc.
Instalação e Configuração
- Instale o pacote
drf-spectacular
:
pdm add drf-spectacular
- Adicione o pacote
drf_spectacular
na lista deINSTALLED_APPS
, nosettings.py
:
INSTALLED_APPS = [
...
"drf_spectacular",
...
]
- Registre o pacote no
settings.py
:
REST_FRAMEWORK = {
...
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
- Faça ainda algumas configurações no
settings.py
:
SPECTACULAR_SETTINGS = {
"TITLE": "Livraria API",
"DESCRIPTION": "API para gerenciamento de livraria, incluindo endpoints e documentação.",
"VERSION": "1.0.0",
}
- Inclua o seguinte conteúdo no arquivo
urls.py
, organizando-o adequadamente:
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
...
urlpatterns = [
...
# OpenAPI 3
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"api/swagger/",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
path(
"api/redoc/",
SpectacularRedocView.as_view(url_name="schema"),
name="redoc",
),
]
Teste
-
Acesse o Swagger:
Alteração da URL da API
- Edite o arquivo
urls.py
altere a URL da API parahttp://0.0.0.0:19003/api/api/
:
urlpatterns = [
...
path("api/", include(router.urls)),
...
]
Vamos aprender a fazer o dump e load de dados.
Carga inicial de dados
-
Acesse o seguinte link:
- Link:
http://191.52.55.156:19005/admin
- Usuário:
a@a.com
- Senha:
senha.123
- Link:
-
Cadastre pelos menos 10 livros, com autor e editora
-
Verifique se o livro, autor ou editora já estão cadastrados.
-
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 python manage.py dumpdata --indent 2 > livraria_bkp.json
- Observe que o arquivo
livraria_bkp.json
foi criado:
code livraria_bkp.json
Arquivo exemplo
- Baixe o arquivo
core.json
:
wget https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/core.json
# Invoke-WebRequest -Uri "https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/core.json" -OutFile core.json # no PowerShell
Carga dos dados
- Execute o comando
loaddata
:
pdm run python manage.py loaddata core.json
- Observe que os dados foram carregados:
pdm run python manage.py shell
>>> from core.models import Livro
>>> Livro.objects.all()
Você também pode acessar o Django Admin ou o Swagger e verificar que os dados foram carregados.
O Django Shell é uma ferramenta para interagir com o banco de dados.
- Acesse o shell:
pdm run python manage.py shell
- Importe os modelos de
core.models
:
>>> from core.models import Autor, Categoria, Editora, Livro
- 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 []>
- 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()
O Admin é uma ferramenta para gerenciar os dados do banco de dados. Ele pode ser customizado para melhorar a experiência do usuário.
Customização do Admin
- Edite o arquivo
livraria/admin.py
:
...
@admin.register(Autor)
class AutorAdmin(admin.ModelAdmin):
list_display = ('nome', 'email')
search_fields = ('nome', 'email')
list_filter = ('nome',)
ordering = ('nome', 'email')
@admin.register(Categoria)
class CategoriaAdmin(admin.ModelAdmin):
list_display = ('descricao',)
search_fields = ('descricao',)
list_filter = ('descricao',)
ordering = ('descricao',)
@admin.register(Editora)
class EditoraAdmin(admin.ModelAdmin):
list_display = ('nome',)
search_fields = ('nome',)
list_filter = ('nome',)
ordering = ('nome',)
@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
-
Acesse o Admin:
Vamos incluir a foto de perfil no usuário.
Criação do campo de foto de perfil
- No arquivo
models/usuario.py
, inclua o campofoto
:
...
from uploader.models import Image
...
class Usuario(AbstractUser):
foto = models.ForeignKey(
Image,
on_delete=models.SET_NULL,
null=True,
blank=True,
default=None,
)
- Faça as migrações:
pdm run python manage.py makemigrations livraria
pdm run python manage.py migrate
- No arquivo
admin.py
, inclua o campofoto
:
...
class UsuarioAdmin(UserAdmin):
...
"fields": ("first_name","last_name","foto",...),
...
- Crie um serializador para o usuário:
from rest_framework.serializers import ModelSerializer, SlugRelatedField
from core.models import Usuario
from uploader.models import Image
from uploader.serializers import ImageSerializer
class UsuarioSerializer(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 = Usuario
fields = "__all__"
- Inclua o novo serializador no arquivo
__init__.py
dos serializadores:
from .usuario import UsuarioSerializer
- Crie uma nova view para o usuário:
from rest_framework.viewsets import ModelViewSet
from core.models import Usuario
from core.serializers import UsuarioSerializer
class UsuarioViewSet(ModelViewSet):
queryset = Usuario.objects.all()
serializer_class = UsuarioSerializer
- Inclua a nova view no arquivo
__init__.py
das views:
from .usuario import UsuarioViewSet
- Inclua a nova view no arquivo
urls.py
:
from core.views import UsuarioViewSet
...
router.register(r"usuarios", UsuarioViewSet)
Testando
- Inclua uma foto de perfil em um usuário.
Nessa aula, vamos criar um model de compras integrada à model do usuário do projeto.
Criando o model de compras
- Crie um novo arquivo
compra.py
dentro da pastamodels
do applivraria
:
touch livraria/models/compra.py
- Inclua o seguinte conteúdo no arquivo
compra.py
:
from django.db import models
from usuario.models import Usuario
class Compra(models.Model):
class StatusCompra(models.IntegerChoices):
CARRINHO = 1, "Carrinho"
REALIZADO = 2, "Realizado"
PAGO = 3, "Pago"
ENTREGUE = 4, "Entregue"
usuario = models.ForeignKey(Usuario, on_delete=models.PROTECT, related_name="compras")
status = models.IntegerField(choices=StatusCompra.choices, default=StatusCompra.CARRINHO)
Note que estamos utilizando o
Usuario
do appusuario
comoForeignKey
para o 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
.
- Inclua a nova model no arquivo
__init__.py
dos models:
from .compra import Compra
- Adicione o model
Compra
aoadmin.py
do applivraria
:
...
from core.models import Compra
admin.site.register(Compra)
- Execute as migrações:
pdm run python manage.py makemigrations
pdm run python manage.py migrate
- Teste o model
Compra
no admin do Django.
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.
- Vamos adicionar um novo model
ItensCompra
ao arquivocompra.py
:
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?)
- Verifique que a tabela
livraria_itenscompra
foi criada no banco de dados. - Inclua o model
ItensCompra
noAdmin
do Django.
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 applivraria
, adicione o seguinte código:
class ItensCompraInline(admin.TabularInline):
model = ItensCompra
@admin.register(Compra)
class CompraAdmin(admin.ModelAdmin):
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.
- Teste no admin do Django.
Vamos criar um endpoint para listagem básica de compras.
Serializer de Compra
- Crie um novo arquivo
compra.py
dentro da pastaserializers
do applivraria
:
touch livraria/serializers/compra.py
- Inclua o seguinte conteúdo no arquivo
compra.py
:
from rest_framework.serializers import ModelSerializer, CharField
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
Viewset de Compra
- Crie um novo arquivo
compra.py
dentro da pastaviews
do applivraria
:
touch livraria/views/compra.py
- Inclua o seguinte conteúdo no arquivo
compra.py
:
from rest_framework.viewsets import ModelViewSet
from core.models import Compra
from core.serializers import CompraSerializer
class CompraViewSet(ModelViewSet):
queryset = Compra.objects.all()
serializer_class = CompraSerializer
- 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 applivraria
:
...
from core.views import AutorViewSet, CategoriaViewSet, CompraViewSet, EditoraViewSet, LivroViewSet
...
router.register(r"compras", CompraViewSet)
...
- Teste o endpoint no navegador.
- Faça o commit e push das alterações.
Inclusão do email do usuário na listagem da compra
- Vamos incluir o email do usuário na listagem da compra.
- No serializer de
Compra
, inclua o seguinte código:
...
...
class CompraSerializer(ModelSerializer):
usuario = CharField(source="usuario.email", read_only=True)
...
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 e push das alterações.
Inclusão do status da compra na listagem da compra
- 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)
...
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 e push das alterações.
Vamos incluir os itens da compra na listagem de compras.
- Crie um serializer para
ItensCompra
:
...
from core.models import Compra, ItensCompra
...
class ItensCompraSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = "__all__"
No ComprasSerializer
, 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 e push das alterações.
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. 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.
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 lista de campos, apenas os campos da lista serão mostrados.
- Teste o endpoint no navegador.
Mostrando mais detalhes do livro na listagem de compras
Utilizando depth = 2, podemos mostrar mais detalhes do livro na listagem de compras.
- No
ItensCompraSerializer
, modifique a linhadepth
:
depth = 2
Nesse caso, vamos ver os detalhes dos livros, como editora, autor e categoria.
- Teste o endpoint no navegador.
- Faça o commit e push das alterações.
Mostrando o total do item na listagem de compras
O total do item é calculado pelo preço do livro multiplicado pela quantidade. Esse é um campo calculado, que não existe no model ItensCompra
. Vamos incluir esse campo na listagem de compras.
- No
ItensCompraSerializer
, inclua o seguinte código:
...
total = SerializerMethodField()
...
def get_total(self, instance):
return instance.quantidade * instance.livro.preco
...
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>
.
- Teste o endpoint no navegador.
- Faça o commit e push das alterações.
Vamos incluir o total da compra na listagem de compras. O total da compra é calculado pela soma dos totais dos itens da compra. Esse é um campo calculado, que não existe no model Compra
. Vamos incluir esse campo na listagem de compras.
- 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 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
. NoComprasSerializer
, 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 e push das alterações.
Vamos primeiro definir o que é necessário para criar uma nova compra. Para criar uma nova compra, precisamos informar o usuário e os itens da compra. Os itens da compra são compostos pelo livro e pela quantidade. Essas são as informações necessárias para criar uma nova compra.
O formato dos dados para criar uma nova compra é o seguinte:
{
"usuario": 1,
"itens": [
{
"livro": 1,
"quantidade": 1
},
{
"livro": 2,
"quantidade": 2
}
]
}
Tendo definido o formato dos dados, vamos criar um novo serializer
, que será usado para criar uma nova compra ou editar uma compra já existente.
- No arquivo
serializer/compra.py
, inclua o seguinte código:
...
class CriarEditarCompraSerializer(ModelSerializer):
itens = ItensCompraSerializer(many=True)
class Meta:
model = Compra
fields = ("usuario", "itens")
...
O parâmetro
many=True
indica que o campoitens
é uma lista de itens de compra.
Vamos alterar o viewset
de Compra
para usar o novo serializer
.
- No arquivo
views/compra.py
altere oviewset
deCompra
para usar o novoserializer
:
...
class CompraViewSet(ModelViewSet):
queryset = Compra.objects.all()
serializer_class = CompraSerializer
def get_serializer_class(self):
if self.action == "create" or self.action == "update":
return CriarEditarCompraSerializer
return CompraSerializer
...
- Tente criar uma nova compra no endpoint
compras/
noThunderClient
, utilizando o métodoPOST
:
{
"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.CriarEditarCompraSerializer`, 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.CriarEditarCompraSerializer`, ou defina `read_only=True` nos campos do serializer aninhado.
O erro ocorre por que os itens da compra vêm de outra tabela, a tabela ItemCompra
, através de uma chave estangeira. O serializer de Compra
não sabe como criar os itens da compra. Precisamos alterar o método create
do serializer
de Compra
para criar os itens da compra.
- No arqiuvo
serializers/compra.py
, altere oserializer
deCompra
para suportar campos aninhados:
...
class CriarEditarCompraSerializer(ModelSerializer):
itens = CriarEditarItensCompraSerializer(many=True) # Aqui mudou
class Meta:
model = Compra
fields = ("usuario", "itens")
def create(self, validated_data):
itens_data = validated_data.pop("itens")
compra = Compra.objects.create(**validated_data)
for item_data in itens_data:
ItensCompra.objects.create(compra=compra, **item_data)
compra.save()
return compra
O método
create
é chamado quando uma nova compra é criada. Ele recebe os dados validados e cria a compra e os itens da compra.
- Precisamos criar também um novo
serializer
para os itens da compra. Noserializers/compra.py
, inclua o seguinte código:
...
class CriarEditarItensCompraSerializer(ModelSerializer):
class Meta:
model = ItensCompra
fields = ("livro", "quantidade")
...
O
serializer
deItensCompra
é bem simples, pois ele apenas recebe o livro e a quantidade.
- Teste o endpoint no `ThunderClient.
- Faça o commit e push das alterações.
- 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.CriarEditarCompraSerializer`, or set `read_only=True` on nested serializer fields.
Traduzindo, chegamos no seguinte:
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.CriarEditarCompraSerializer`, ou defina `read_only=True` nos campos do serializer aninhado.
O erro ocorre por que os itens da compra vêm de outra tabela, a tabela
ItensCompra
, através de uma chave estangeira. O serializer deCompra
não sabe como atualizar os itens da compra. Precisamos alterar o métodoupdate
doserializer
deCompra
para atualizar os itens da compra.
- No arquivo
serializers/compra.py
, altere oserializer
deCompra
para suportar campos aninhados:
...
def update(self, instance, validated_data):
itens = validated_data.pop("itens")
if itens:
instance.itens.all().delete()
for item in itens:
ItensCompra.objects.create(compra=instance, **item)
instance.save()
return instance
...
O método
update
é chamado quando uma compra é atualizada. Ele recebe os dados validados e atualiza a compra e os itens da compra.
O método
update
recebe dois parâmetros:instance
evalidated_data
. Oinstance
é a compra que está sendo atualizada. Ovalidated_data
são os dados validados que estão sendo atualizados.
O método
update
remove todos os itens da compra (se houverem) e cria novos itens com os dados validados.
O método
update
salvamos a compra e retornamos a instância.
- Teste 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;
- use o método
- Faça o commit e push das alterações.
Ao invés de passar o usuário no corpo da requisição, podemos pegar o usuário autenticado e criar a compra a partir dele. O Django Rest Framework
nos dá uma forma de fazer isso.
- Primeiro, vamos importar todos os
serializers
derest_framework
emserializers/compra.py
:
from rest_framework import serializers
- Agora, vamos definir o usuário como um campo oculto, cujo valor padrão é o usuário autenticado:
class ComprasSerializer(ModelSerializer):
itens = ItensCompraSerializer(many=True)
usuario = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Compra
fields = ("id", "usuario", "status", "total", "itens")
O campo
usuario
é um campo oculto, pois foi definido comoserializers.HiddenField
. Ele não é exibido noserializer
.
O valor padrão do campo é o usuário autenticado.
O
CurrentUserDefault
é um campo padrão que retorna o usuário autenticado.
Para testar, vamos criar uma nova compra no endpoint compras/
no ThunderClient
, utilizando o método POST
:
{
"itens": [
{
"livro": 2,
"quantidade": 2
}
]
}
Observe que não precisamos mais passar o usuário no corpo da requisição, pois ele pega o usuário autenticado.
- Faça o commit e push das alterações.
Nesse momento, qualquer usuário pode ver todas as compras. Vamos filtrar apenas as compras do usuário autenticado.
- No
views.py
, vamos alterar oviewset
deCompra
para filtrar apenas as compras do usuário autenticado:
...
class CompraViewSet(ModelViewSet):
queryset = Compra.objects.all()
def get_queryset(self):
usuario = self.request.user
if usuario.is_superuser:
return Compra.objects.all()
if usuario.groups.filter(name="Administradores"):
return Compra.objects.all()
return Compra.objects.filter(usuario=usuario)
...
O método
get_queryset
é chamado quando uma compra é listada. Ele retorna apenas as compras do usuário autenticado. Orequest
é o objeto que representa a requisição. Orequest.user
é o usuário autenticado. Se o usuário for superusuário ou for membro do grupo "Administradores", retorna todas as compras.
- Para testar, autentique-se com um usuário normal e depois com um que seja administrador. Você verá que o administrador consegue ver todas as compras, enquanto o usuário normal só consegue ver as suas compras.
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 oserializer
CriarEditarItensCompraSerializer
para validar a quantidade de itens em estoque:
...
def validate(self, data):
if data["quantidade"] > data["livro"].quantidade:
raise serializers.ValidationError(
{"quantidade": "Quantidade solicitada não disponível em estoque."}
)
return data
...
- Para testar, tente criar uma compra com uma quantidade de itens maior do que a quantidade em estoque. Você verá que a compra não é criada e é exibida uma mensagem de erro.
- Faça o commit e push das alterações.
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.
- Primeiro, precisamos incluir o campo
preco_item
no modelItensCompra
:
...
class ItensCompra(models.Model):
...
preco_item = models.DecimalField(max_digits=10, decimal_places=2, default=0)
...
-
Execute as migrações.
-
No
serializers/compra.py
, vamos alterar a funçãocreate
doserializer
CriarEditarCompraSerializer
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"] = item["livro"].preco # Coloca o preço do livro no item de compra
ItensCompra.objects.create(compra=compra, **item)
compra.save()
return compra
...
O método
create
é chamado quando uma nova compra é criada. Ele recebe os dados validados e cria a compra e os itens da compra.
- Para finalizar, precisamos alterar o campo total da compra para considerar o preço do item da compra:
...
@property
def total(self):
return sum(item.preco_item * item.quantidade for item in self.itens.all())
...
Estamos utilizando o campo
preco_item
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.
- Faça o commit e push das alterações.
Da mesma forma, podemos alterar o método update
do serializer
CriarEditarCompraSerializer
para gravar o preço do livro no item da compra:
...
def update(self, instance, validated_data):
itens = validated_data.pop("itens")
if itens:
instance.itens.all().delete()
for item in itens:
item["preco_item"] = item["livro"].preco # Coloca o preço do livro no item de compra
ItensCompra.objects.create(compra=instance, **item)
instance.save()
return instance
...
- Para testar, altere uma compra e verifique que o preço do livro foi gravado no item da compra.
- Faça o commit e push das alterações.
Nesse momento, é possível apenas listar todos os livros. Vamos ver como podemos filtrar os livros por seus atributos, como categoria
, editora
e autores
.
Vamos começar filtrando os livros por categoria. Para isso, utilizaremos o pacote django-filter
:
- Instale o pacote
django-filter
:
pdm add django-filter
- No
settings.py
, inclua odjango-filter
noINSTALLED_APPS
:
...
INSTALLED_APPS = [
...
"django_filters",
...
]
...
- 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
. Ofilterset_fields
indica quais campos serão filtrados. Nesse caso, estamos filtrando apenas pelo campocategoria
.
- 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://127.0.0.1:8000/api/livros/?categoria=Python
. Você verá que apenas os livros da categoria informada foram listados. - Faça o commit e push das alterações.
Vamos acrescentar outros filtros na listagem de livros.
- No
views/livro.py
, vamos alterar o atributofilterset_fields
, naviewset
deLivro
para filtrar os livros porcategoria
eeditora
:
...
filterset_fields = ["categoria__descricao", "editora__nome"]
...
- Para filtrar por categoria e editora:
- Para filtrar apenas por editora:
Exercício
- Acrescente filtros nas models
Autor
,Categoria
,Editora
eCompra
.
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: -
Faça o commit e push das alterações.
Exercício
- Acrescente a busca textual nas models
Autor
,Categoria
,Editora
eCompra
.
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. Por exemplo, para ordenar os livros pelo título, a URL ficaria assim: -
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:
- http://127.0.0.1:8000/api/livros/?categoria=1&editora=1&ordering=titulo
- É possível utilizar todos os recursos ao mesmo tempo: múltiplos filtros, busca textual e ordenação.
-
Faça o commit e push das alterações.
Exercício
- Acrescente a ordenação nas models
Autor
,Categoria
,Editora
eCompra
.
Nesse momento, não temos a data da compra. Vamos incluir a data da compra, utilizando a data e hora atual no momento da criação da compra.
- No
models.py
, vamos incluir o campodata
no modelCompra
:
...
class Compra(models.Model):
usuario = models.ForeignKey(User, on_delete=models.PROTECT)
status = models.IntegerField(choices=StatusCompra.choices, default=StatusCompra.ABERTA)
data = models.DateTimeField(auto_now_add=True)
...
O campo
data
é um campo do tipoDateTimeField
, que armazena a data e a hora da compra. O parâmetroauto_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.
- Para testar, crie uma nova compra e verifique que a data da compra foi gravada.
- Faça o commit e push das alterações.
Modificando o serializer de compra para mostrar a data da compra
- No
serializers/compra.py
, vamos incluir o campodata
noserializer
deCompra
:
...
class ComprasSerializer(ModelSerializer):
itens = ItensCompraSerializer(many=True)
usuario = serializers.HiddenField(default=serializers.CurrentUserDefault())
data = serializers.DateTimeField(read_only=True)
class Meta:
model = Compra
fields = ("id", "usuario", "status", "total", "data", "itens")
...
Acrescentando filtro e ordenação por data
- 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
.
...
filterset_fields = ["usuario", "status", "data"]
ordering_fields = ["usuario", "status", "data"]
...
- Para ordenar por data, em ordem descrente:
Nesse momento, a API retorna todos os registros de uma vez. Vamos adicionar paginação à API.
- No objeto
REST_FRAMEWORK
, nosettings.py
, vamos incluir os seguintes parâmetros:
REST_FRAMEWORK = {
...
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
...
}
Feito isso, o resultado da API será paginado, com 10 registros por página.
O formato do resultado da API será o seguinte:
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"usuario": 1,
"status": 1,
"total": "100.00",
"data": "2021-08-22T21:00:00Z",
"itens": [
{
"id": 1,
"livro": 1,
"quantidade": 1,
"preco_item": "100.00",
"total": "100.00"
}
]
},
{
"id": 2,
"usuario": 1,
"status": 1,
"total": "100.00",
"data": "2021-08-22T21:00:00Z",
"itens": [
{
"id": 2,
"livro": 1,
"quantidade": 1,
"preco_item": "100.00",
"total": "100.00"
}
]
}
]
}
O campo
count
indica a quantidade de registros retornados.
O campo
next
indica a url da próxima página.
O campo
previous
indica a url da página anterior.
O campo
results
indica os registros retornados.
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
ou outros
.
- 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.
- Para testar, crie uma nova compra e verifique que o tipo de pagamento foi gravado.
- Faça o commit e push das alterações.
Uma forma de diferenciar os usuários é através do tipo de usuário. Vamos adicionar o tipo de usuário à model de usuário.
Outra abordagem possível é utilizar o Group
do Django.
- No
models\usuario.py
, vamos incluir o campotipo_usuario
no modelUsuario
:
...
class Usuario(AbstractUser):
class TipoUsuario(models.IntegerChoices):
CLIENTE = 1, "Cliente"
VENDEDOR = 2, "Vendedor"
GERENTE = 3, "Gerente"
...
tipo_usuario = models.IntegerField(_("User Type"), choices=TipoUsuario.choices, default=TipoUsuario.CLIENTE)
...
O campo
tipo_usuario
é um campo do tipoIntegerField
, que armazena o tipo de usuário. O parâmetrochoices
indica as opções de usuário. O parâmetrodefault
indica o tipo de usuário padrão.
- Execute as migrações.
- Para testar, crie um novo usuário e verifique que o tipo de usuário foi gravado.
Uma forma de utilizar o tipo de usuário é verificando se o usuário é GERENTE
e então permitir que ele tenha acesso a todas as compras. Vamos ver como fazer isso.
- No
views/compra.py
, vamos alterar o métodoget_queryset
para permitir que o usuárioGERENTE
tenha acesso a todas as compras:
...
class CompraViewSet(ModelViewSet):
...
def get_queryset(self):
usuario = self.request.user
...
if usuario.tipo == Usuario.Tipos.GERENTE:
return Compra.objects.all()
return Compra.objects.filter(usuario=usuario)
...
O método
get_queryset
é chamado quando uma compra é listada. Ele retorna apenas as compras do usuário autenticado, exceto se o usuário forGERENTE
, que retorna todas as compras.
- Para testar, autentique-se com um usuário normal e depois com um que seja
GERENTE
. Você verá que oGERENTE
consegue ver todas as compras, enquanto o usuário normal só consegue ver as suas compras. - Faça o commit e push das alterações.
O projeto Garagem é um projeto de uma garagem de carros. O objetivo é praticar aquilo que foi visto nesse tutorial, no projeto core.
Seguindo aquilo que você já aprendeu na criação do projeto da Livraria
, crie um novo projeto, a partir do template.
- O projeto será chamado
Garagem
. - Nomeie o commit como sendo
Criação do projeto
. - Crie as seguintes APIs, fazendo um commit para cada uma:
Acessório
:descricao
(string, máximo 100 caracteres).__str__
(retorna a descrição e o id).
Categoria
:descricao
(string, máximo 100 caracteres).__str__
(retorna a descrição e o id.
Cor
:nome
(string, máximo 100 caracteres).__str__
(retorna o nome e o id).
Marca
:nome
(string, máximo 50 caracteres).nacionalidade
(string, máximo 50 caracteres, permite nulo).__str__
(retorna o nome em caixa alta e o id).
- Crie a aplicação frontend com Vuejs para consumir a API REST do projeto
Garagem
. Pode utilizar o template do projeto dalivraria-vue3
como base.
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:
- Black Formatter(Formatação de código)
- ESLint (JavaScript)
- Even Better TOML (Melhorias na edição de arquivos TOML)
- Intellicode (Desenvolvimento Inteligente)
- isort (Organização de imports)
- Markdown All in One (Edição de arquivos Markdown)
- Material Icon Theme (Temas de ícones)
- 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!)
- SqLite Viewer (Visualização de bancos de dados SQLite)
- Thunder Client (Teste de APIs)
- TODO Highlight (Realce de TODOs)
- Vue Language Features (Desenvolvimento de aplicações Vue.js)
Extensão Vue.js devtools no Google Chrome
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 -sSL 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 -sSL https://github.com/marrcandre/django-drf-tutorial/raw/main/scripts/pdm_config_bash.sh | bash
Configuração do PDM no zsh
com o Oh! My Zsh
(Manjaro e derivados)
- 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 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.
- Escolha a opção
Project settings
e depoisDatabase
. - Copia a linha de conexão do banco de dados (URI).
- Ela deve ser parecida com isso:
postgres://postgres:[YOUR-PASSWORD]@!@db.vqcprcexhnwvyvewgrin.supabase.co:5432/postgres
.
- Ela deve ser parecida com isso:
- Troque
[YOUR-PASSWORD
pela senha que você havia guardado. - Copie a linha de conexão e cole no arquivo
.env
do projeto, como no exemplo:
# Supabase
DATABASE_URL=postgres://postgres:Senha.123@!@db.vqcprcexhnwvyvewgrin.supabase.co:5432/postgres
Instalando o pacote dj_database_url
O pacote dj_database_url
facilita a configuração do banco de dados no Django, pois ele converte a URL do banco de dados para o formato que o Django entende.
- Instale o pacote
dj_database_url
:
pdm add dj-database-url
- Adicione o pacote
dj_database_url
ao arquivosettings.py
:
import dj_database_url
- Substitua a configuração do banco de dados no arquivo
settings.py
:
DATABASES = {
'default': dj_database_url.config(
default='sqlite:///db.sqlite3',
conn_max_age=600,
conn_health_checks=True,
)
}
Essa configuração permite que o Django use o banco de dados local em desenvolvimento e o banco de dados do Supabase em produção, definindo a variável de ambiente
DATABASE_URL
.
Migrando o banco de dados
- No arquivo
.env
:- Altere o valor da variável
MODE
paraMIGRATE
. - Descomente a linha
DATABASE_URL
.
- Altere o valor da variável
- Faça a migracão do banco de dados:
pdm run python 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
.
Utilizando o banco de dados local
- Para voltar a usar o banco de dados local, no arquivo
.env
:- Altere o valor da variável
MODE
paraDEVELOPMENT
. - Comente a linha
DATABASE_URL
.
- Altere o valor da variável
IMPORTANTE: A cada nova alteração no banco de dados, você deve repetir esse processo de migração, tanto no banco de dados local quanto no banco de dados do Supabase.
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
).
- Crie um arquivo chamado
build.sh
na raiz do projeto com o seguinte conteúdo:
#!/usr/bin/env bash
# Sai do script se houver algum erro
set -o errexit
# Atualiza o pip
pip install --upgrade pip
# Instala as dependências
pip install -r requirements.txt
# Coleta os arquivos estáticos
python manage.py collectstatic --no-input
# Aplica as migrações
python manage.py migrate
- Torne o script executável:
chmod a+x build.sh
- Adicione os pacotes
Uvicorn
eGunicorn
ao projeto:
pdm add uvicorn gunicorn
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
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. Detsa 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, faça o commit das alterações.
- Sua aplicação deve estar funcionando normalmente, utilizando o
Cloudinary
para armazenar os arquivos estáticos.
- 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 8000/tcp
Este comando vai matar o processo que está rodando na porta 8000. 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 esse 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 email no git. Para isso, abra o terminal do VS Code e digite:
git config --global user.name "Seu Nome"
git config --global user.email "seuEmailNoGitHub@gmail.com"
- 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 email.
- 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/
Para contriburi com esse 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 <marcoandre@gmail.com>