Aula 96 – Django – Ecommerce – Refatoração do Projeto
Aula 96 – Django – Ecommerce – Refatoração do Projeto
Voltar para página principal do blog
Todas as aulas desse curso
Aula 95 Aula 97
Redes Sociais do Código Fluente:
Scarlett Finch
Scarlett Finch é uma influenciadora virtual criada com IA.
Ela é 🎤 cantora e 🎶compositora pop britânica.
Siga a Scarlett Finch no Instagram:
Conecte-se comigo!
LinkedIn: Fique à vontade para me adicionar no LinkedIn.
Ao conectar-se comigo, você terá acesso a atualizações regulares sobre desenvolvimento web, insights profissionais e oportunidades de networking no setor de tecnologia.
GitHub: Siga-me no GitHub para ficar por dentro dos meus projetos mais recentes, colaborar em código aberto ou simplesmente explorar os repositórios que eu contribuo, o que pode ajudar você a aprender mais sobre programação e desenvolvimento de software.
Recursos e Afiliados
Explorando os recursos abaixo, você ajuda a apoiar nosso site.
Somos parceiros afiliados das seguintes plataformas:
- https://heygen.com/ – Eleve a produção de seus vídeos com HeyGen! Com esta plataforma inovadora, você pode criar vídeos envolventes utilizando avatares personalizados, ideal para quem busca impactar e conectar com audiências em todo o mundo. HeyGen transforma a maneira como você cria conteúdo, oferecendo ferramentas fáceis de usar para produzir vídeos educativos, demonstrações de produtos e muito mais. Descubra o poder de comunicar através de avatares interativos e traga uma nova dimensão para seus projetos. Experimente HeyGen agora e revolucione sua forma de criar vídeos!
- letsrecast.ai – Redefina a maneira como você consome artigos com Recast. Esta plataforma transforma artigos longos em diálogos de áudio que são informativos, divertidos e fáceis de entender. Ideal para quem está sempre em movimento ou busca uma forma mais conveniente de se manter informado. Experimente Recast agora.
- dupdub.com – Explore o universo do marketing digital com DupDub. Esta plataforma oferece ferramentas inovadoras e soluções personalizadas para elevar a sua estratégia de marketing online. Ideal para empresas que buscam aumentar sua visibilidade e eficiência em campanhas digitais. Descubra mais sobre DupDub.
- DeepBrain AI Studios – Revolucione a criação de conteúdo com a tecnologia de inteligência artificial da DeepBrain AI Studios. Esta plataforma avançada permite que você crie vídeos interativos e apresentações utilizando avatares digitais gerados por IA, que podem simular conversas reais e interações humanas. Perfeito para educadores, criadores de conteúdo e empresas que querem inovar em suas comunicações digitais. Explore DeepBrain AI Studios.
- Audyo.ai – Transforme a maneira como você interage com conteúdo auditivo com Audyo.ai. Esta plataforma inovadora utiliza inteligência artificial para criar experiências de áudio personalizadas, melhorando a acessibilidade e a compreensão de informações através de podcasts, transcrições automáticas e síntese de voz avançada. Ideal para profissionais de mídia, educadores e qualquer pessoa que deseje acessar informações auditivas de maneira mais eficiente e envolvente. Descubra Audyo.ai e suas possibilidades.
- Acoust.io – Transforme sua produção de áudio com Acoust.io. Esta plataforma inovadora fornece uma suite completa de ferramentas para criação, edição e distribuição de áudio, ideal para artistas, produtores e empresas de mídia em busca de excelência e inovação sonora. Acoust.io simplifica o processo de levar suas ideias à realidade, oferecendo soluções de alta qualidade que elevam seus projetos de áudio. Experimente Acoust.io agora e descubra um novo patamar de possibilidades para seu conteúdo sonoro.
- Hostinger – Hospedagem web acessível e confiável. Ideal para quem busca soluções de hospedagem de sites com excelente custo-benefício e suporte ao cliente robusto. Saiba mais sobre a Hostinger.
- Digital Ocean – Infraestrutura de nuvem para desenvolvedores. Oferece uma plataforma de nuvem confiável e escalável projetada especificamente para desenvolvedores que precisam de servidores virtuais, armazenamento e networking. Explore a Digital Ocean.
- One.com – Soluções simples e poderosas para o seu site. Uma escolha ideal para quem busca registrar domínios, hospedar sites ou criar presença online com facilidade e eficiência. Visite One.com.
Educação e Networking
Amplie suas habilidades e sua rede participando de cursos gratuitos e comunidades de desenvolvedores:
- Digital Innovation One – Cursos gratuitos com certificado.
- Workover – Aprenda Python3 gratuitamente.
- Comunidades de desenvolvedores para networking:
Canais do Youtube
Explore nossos canais no YouTube para uma variedade de conteúdos educativos e de entretenimento, cada um com um foco único para enriquecer sua experiência de aprendizado e lazer.
Toti
Toti: Meu canal pessoal, onde posto clips artesanais de músicas que curto tocar, dicas de teoria musical, entre outras coisas.
Scarlett Finch
Scarlett Finch: Cantora e influenciadora criada com IA.
Lofi Music Zone Beats
Lofi Music Zone Beats: O melhor da música Lofi para estudo, trabalho e relaxamento, criando o ambiente perfeito para sua concentração.
Backing Track / Play-Along
Backing Track / Play-Along: Acompanhe faixas instrumentais para prática musical, ideal para músicos que desejam aprimorar suas habilidades.
Código Fluente
Código Fluente: Aulas gratuitas de programação, devops, IA, entre outras coisas.
Putz!
Putz!: Canal da banda Putz!, uma banda virtual, criada durante a pandemia com mais 3 amigos, Fábio, Tatá e Lula.
PIX para doações
Aula 96 – Django – Ecommerce – Refatoração do Projeto
Código da aula: Github
O que vamos fazer nessa refatoração?
Refatorações Estruturais:
- Atualização para Bootstrap 5.3.0
- Reorganização dos arquivos JavaScript
- Implementação do modelo CartProduct
- Adição do sistema de categorias
Refatorações Funcionais:
- Integração Stripe completa
- Fluxo de checkout refinado
- Gerenciamento de quantidade no carrinho
- Sistema de endereços aprimorado
Outras:
- Setup inicial
- Migrações
- População do banco
- Estrutura de arquivos/pastas
Instruções para Clonar, Configurar e Popular o Banco de Dados
Se você está começando agora e quer acompanhar o projeto exatamente como ele está no momento, temos uma opção prática para você: basta clonar o repositório do projeto, configurar as dependências, criar o banco de dados e populá-lo com dados já preparados.
Assim, você não precisa construir tudo do zero e pode acompanhar as aulas com o projeto já funcionando.
Passos para clonar e configurar o projeto:
Clone o repositório:No terminal ou PowerShell, execute:
git clone https://github.com/toticavalcanti/django_ecommerce/tree/ecommerce-refactoring
Acesse a pasta do projeto clonado:
cd e_commerce
Crie e ative um ambiente virtual:
Linux/Mac
Crie
python3 -m venv venv
Ative
source venv/bin/activate
Powershell/windows
Crie
python -m venv venv
Ative
.\venv\Scripts\Activate.ps1
Permita a execução de scripts
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Instale as dependências:
pip install -r requirements.txt
Remova o banco SQLite existente (opcional): Para garantir que você está começando com um banco de dados limpo, remova o arquivo db.sqlite3
da raiz do projeto:
del db.sqlite3
(Se você não estiver no Windows, ajuste o comando conforme necessário, como rm db.sqlite3
no Linux ou macOS.)
Deletar as Migrações Antigas
Windows
Get-ChildItem -Path .\**\migrations\ -Filter "*.py" -Recurse | Where-Object { $_.Name -ne "__init__.py" } | Remove-Item -Force
Linux e Mac
find . -path "*/migrations/*.py" ! -name "__init__.py" -type f -exec rm -f {} +
Crie o banco de dados:Rode as migrações para criar a estrutura do banco:
python manage.py makemigrations
python manage.py migrate
Popule o Banco
No linux ou mac:
python manage.py dbshell < script.sql
No windows, no powershell:
Get-Content -Encoding UTF8 script.sql | sqlite3 db.sqlite3
Substitua script.sql
pelo nome e caminho do arquivo SQL que você quer aplicar.
O arquivo script.sql
deve estar na raiz do projeto para que o comando da forma como tá acima funcione. Ele irá executar todas as instruções contidas no script.
Colete os Arquivos Estáticos
python manage.py collectstatic
Inicie o servidor
python manage.py runserver
Stripe
Certifique-se que está logado no Stripe
Faça login com sua conta Stripe
stripe login
Encaminhe eventos ao seu webhook
stripe listen --forward-to localhost:4242/api/webhook
Pronto! O projeto está configurado, o banco de dados está populado, o Stripe já tá ouvindo os eventos em modo teste e você pode acompanhar a aula sem problemas e fazer todos os testes.
Refatorando o Projeto
Começaremos atualizando o projeto para usar a versão 5.3.0 do bootstrap.
Atualização para Bootstrap 5
Explicação do base.html – e_commerce/templates/base/css.html
Atualização para a versão 5.3.0 do Bootstrap e do font-awesome para a 6.0.0.
e_commerce/templates/base.html
{% load static %}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Base Template</title>
{% include 'base/css.html' %}
{% block base_head %}{% endblock base_head %}
</head>
<body>
{% include 'base/navbar.html' with nome_da_marca='Vitrine Digital' %}
<div class='container'>
{% block content %} {% endblock %}
</div>
{% include 'base/js.html' %}
</body>
</html>
Explicação do css.html – e_commerce/templates/base/css.html
Atualização para a versão 5.3.0 do Bootstrap e do font-awesome para a 6.0.0.
e_commerce/templates/base/css.html
{% raw %}{% load static %}{% endraw %}
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Atualizado Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<!-- Jquery-confirm CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.css">
<!-- eCommerce Custom Stripe Styles -->
<link rel="stylesheet" href="{% raw %}{% static 'css/stripe-custom-styles.css' %}{% endraw %}">
Explicação do Custom Filters do Carts – e_commerce/templates/base/js.html
- O template carrega a tag
{% load static %}
, usada para resolver caminhos de arquivos estáticos no Django. - Inclui o jQuery através de um link externo (
https://code.jquery.com/jquery-3.6.4.min.js
), com o atributocrossorigin="anonymous"
para permitir solicitações seguras. - Adiciona o Bootstrap e outros scripts dependentes do jQuery, como:
bootstrap.bundle.min.js
para funcionalidades do Bootstrap.jquery-confirm.min.js
para criar caixas de diálogo modais estilizadas.
- Inclui o script da API Stripe para integração de pagamentos, utilizando o link oficial
https://js.stripe.com/v3/
. - Carrega arquivos JavaScript modulares através da tag
{% static %}
, que mapeia os caminhos estáticos do projeto:stripe-payment.js
para lógica de pagamentos com Stripe.search.js
para funcionalidades de busca no e-commerce.contact.js
para ações relacionadas ao formulário de contato.cart.js
para manipulação do carrinho de compras.csrf.ajax.js
para gerenciar tokens CSRF em requisições AJAX.
e_commerce/templates/base/js.html
{% load static %}
<!-- Carregando jQuery sem o atributo 'integrity' -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js" crossorigin="anonymous"></script>
<!-- Bootstrap e outros scripts que dependem de jQuery -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.js"></script>
<!-- Stripe JS -->
<script src="https://js.stripe.com/v3/"></script>
<!-- Modular JS Files -->
<script src="{% static 'js/stripe/stripe-payment.js' %}"></script>
<script src="{% static 'js/ecommerce/search.js' %}"></script>
<script src="{% static 'js/ecommerce/contact.js' %}"></script>
<script src="{% static 'js/ecommerce/cart.js' %}"></script>
<script src="{% static 'js/csrf.ajax.js' %}"></script>
Criando as Categorias
Por que a gente cria categorias?
As categorias ajudam a organizar os produtos na loja.
Elas fazem com que o cliente encontre o que procura mais rápido e de uma maneira bem mais eficiente.
Imagina um supermercado sem prateleiras organizadas? Seria um caos!
Além disso, as categorias também ajudam no SEO, ou seja, na forma como os produtos aparecem nas buscas do Google.
Organizar bem tudo no backend é essencial para melhorar a experiência do usuário e também a visibilidade da loja.
Então vamos criar mais um app no nosso projeto, o app categories.
python manage.py startapp categories
O que está rolando nesse código logo abaixo, o category_list.html?
Esse arquivo é onde a gente mostra todas as categorias disponíveis no nosso site.
Ele pega os dados das categorias e exibe em uma lista.
E claro, como tudo é baseado no base.html
, a estrutura fica igual em todas as páginas do site.
e_commerce/categories/templates/categories/category_list.html
{% extends "base.html" %}
{% block content %}
<h1>Categories</h1>
<ul>
{% for category in categories %}
<li>
<a href="{{ category.get_absolute_url }}">{{ category.name }}</a>
</li>
{% endfor %}
</ul>
{% endblock %}
E no category_detail.html?
Aqui, a gente mostra os detalhes de uma categoria específica.
Exibimos o nome da categoria, a descrição e, claro, listamos os produtos dessa categoria para o usuário ver tudo o que está disponível ali.
e_commerce/categories/templates/categories/category_detail.html
{% extends "base.html" %}
{% block content %}
<h1>{{ category.name }}</h1>
<p>{{ category.description }}</p>
<h2>Products</h2>
<ul>
{% for product in products %}
<li>
<a href="{{ product.get_absolute_url }}">{{ product.title }}</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Aqui nesse código logo abaixo, a gente cria o modelo de Categoria.
Esse arquivo define o que é uma categoria e como ela é armazenada no banco de dados.
Temos o nome da categoria, a descrição, o slug
(que é a versão amigável da URL) e até a imagem da categoria.
e_commerce/categories/models.py
from django.db import models
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=120, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(null=True, blank=True)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
related_name='children',
on_delete=models.CASCADE
)
image = models.ImageField(upload_to='categories/', null=True, blank=True)
active = models.BooleanField(default=True)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Category"
verbose_name_plural = "Categories"
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("categories:detail", kwargs={"slug": self.slug})
O código a seguir são as Views responsáveis pela exibição das categorias.
Nele estão as funções que pegam as categorias do banco de dados e as enviam para o template para serem exibidas.
Usamos o ListView
para exibir todas as categorias e o DetailView
para mostrar os detalhes de uma categoria específica.
e_commerce/categories/views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView
from .models import Category
from products.models import Product
class CategoryListView(ListView):
model = Category
template_name = "categories/category_list.html"
context_object_name = "categories"
class CategoryDetailView(DetailView):
model = Category
template_name = "categories/category_detail.html"
context_object_name = "category"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['products'] = Product.objects.filter(category=self.object, active=True)
return context
Aqui a gente faz a configuração para o Django Admin.
Esse arquivo ajusta como as categorias vão aparecer no painel de administração do Django, permitindo que a gente adicione, edite ou exclua categorias de forma prática.
e_commerce/categories/admin.py
from django.contrib import admin
from .models import Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'parent', 'active', 'timestamp')
list_filter = ('active', 'parent') # Filtros para facilitar a navegação
search_fields = ('name', 'slug', 'description') # Campos para busca
prepopulated_fields = {'slug': ('name',)} # Gera o slug automaticamente
E aqui a gente define as URLs para acessar as categorias.
Esse arquivo mapeia as URLs para a lista de categorias e para os detalhes de cada categoria, permitindo que o usuário acesse a página que ele deseja.
e_commerce/categories/urls.py
from django.urls import path
from .views import CategoryListView, CategoryDetailView
app_name = "categories"
urlpatterns = [
path('', CategoryListView.as_view(), name='list'),
path('<slug:slug>/', CategoryDetailView.as_view(), name='detail'),
]
Explicação do Código – e_commerce/products/views.py
- Os comentários descritivos nos cabeçalhos das classes e funções continuam a servir para documentar o propósito de cada uma, como “Detalhes de um produto utilizando slug como identificador”. Esses comentários facilitam a leitura e manutenção do código.
- O uso de
Product.objects.featured()
centraliza a lógica de exibição de produtos destacados, ajudando a manter o código organizado e a filtrar apenas produtos ativos e em destaque. - Em
context = super().get_context_data(*args, **kwargs)
, o contexto padrão fornecido pela view pai é recuperado e enriquecido com informações adicionais, como o carrinho do usuário (cart
), para ser usado nos templates. - A linha
slug = self.kwargs.get('slug')
foi destacada porque obtém o parâmetroslug
da URL para buscar o produto correspondente no banco de dados. Essa mudança destaca a busca explícita peloslug
, que é um identificador único para cada produto. - A verificação
if self.request.user.is_authenticated
comObjectViewed.objects.create(...)
garante que apenas usuários autenticados tenham suas visualizações de produtos registradas. Isso é útil para análises de comportamento ou personalização de conteúdo.
e_commerce/products/views.py
from django.http import Http404
from django.views.generic import ListView, DetailView
from django.shortcuts import render, get_object_or_404
from analytics.models import ObjectViewed
from analytics.mixin import ObjectViewedMixin
from carts.models import Cart
from .models import Product
class ProductFeaturedListView(ListView):
"""Listagem de produtos destacados."""
template_name = "products/list.html"
def get_queryset(self, *args, **kwargs):
return Product.objects.featured()
class ProductFeaturedDetailView(ObjectViewedMixin, DetailView):
"""Detalhes de um produto destacado."""
queryset = Product.objects.featured()
template_name = "products/featured-detail.html"
class ProductListView(ListView):
"""Listagem de produtos disponíveis."""
queryset = Product.objects.all()
template_name = "products/list.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
cart_obj, new_obj = Cart.objects.new_or_get(self.request)
context['cart'] = cart_obj
return context
def product_list_view(request):
"""Listagem de produtos usando Function-Based View."""
queryset = Product.objects.all()
context = {
'object_list': queryset
}
return render(request, "products/list.html", context)
class ProductDetailSlugView(ObjectViewedMixin, DetailView):
"""Detalhes de um produto utilizando slug como identificador."""
queryset = Product.objects.all()
template_name = "products/detail.html"
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
cart_obj, new_obj = Cart.objects.new_or_get(self.request)
context['cart'] = cart_obj
return context
def get_object(self, *args, **kwargs):
slug = self.kwargs.get('slug')
try:
instance = Product.objects.get(slug=slug, active=True)
except Product.DoesNotExist:
raise Http404("Produto não encontrado!")
except Product.MultipleObjectsReturned:
qs = Product.objects.filter(slug=slug, active=True)
instance = qs.first()
# Cria o evento ObjectViewed se o usuário estiver autenticado
if self.request.user.is_authenticated:
ObjectViewed.objects.create(
user=self.request.user,
content_object=instance
)
return instance
class ProductDetailView(ObjectViewedMixin, DetailView):
"""Detalhes de um produto utilizando o slug como identificador."""
template_name = "products/detail.html"
model = Product # Define o modelo diretamente
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
cart_obj, new_obj = Cart.objects.new_or_get(self.request)
context['cart'] = cart_obj
return context
def get_object(self, *args, **kwargs):
slug = self.kwargs.get('slug') # Busca pelo slug na URL
instance = Product.objects.filter(slug=slug).first()
if instance is None:
raise Http404("Esse produto não existe!")
return instance
def product_detail_view(request, pk=None, *args, **kwargs):
"""Detalhes de um produto utilizando Function-Based View."""
instance = Product.objects.get_by_id(pk)
if instance is None:
raise Http404("Esse produto não existe!")
context = {
'object': instance
}
return render(request, "products/detail.html", context)
Explicação do Código – e_commerce/products/urls.py
- Importação de
ProductDetailView
: Antes, a view usada para detalhes eraProductDetailSlugView
. Agora, foi substituída porProductDetailView
, refletindo uma possível mudança no modo como os detalhes dos produtos são tratados. - Adição da rota para listar produtos por categoria: Uma nova URL foi adicionada:
category/<slug:slug>/
. Essa rota permite que os produtos sejam filtrados e exibidos com base em uma categoria específica, utilizando a mesma viewProductListView
para gerenciar a lógica. A adição dessa rota sugere que agora o sistema suporta categorias para organizar os produtos. - Substituição de
ProductDetailSlugView
porProductDetailView
: A rota para detalhes de produtos (<slug:slug>/
) agora utiliza a nova viewProductDetailView
. Isso pode indicar uma refatoração ou ajuste na lógica de exibição dos detalhes de um produto.
e_commerce/products/urls.py
<!-- Path: products/urls.py -->
from django.urls import path
from .views import ProductListView, ProductDetailView
app_name = "products"
urlpatterns = [
path('', ProductListView.as_view(), name='list'),
path('category/<slug:slug>/', ProductListView.as_view(), name='category'),
path('<slug:slug>/', ProductDetailView.as_view(), name='detail'),
]
Explicação do Código – e_commerce/e_commerce/urls.py
path('login/', LoginView.as_view(), name='login')
:
- Define uma rota para o endpoint de login da aplicação.
- Utiliza a
LoginView
(umaFormView
) para exibir e processar o formulário de login. - O nome da rota é
'login'
, permitindo que ela seja referenciada facilmente em templates ou redirecionamentos usando{% url 'login' %}
oureverse('login')
.
e_commerce/e_commerce/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth.views import LogoutView
from django.urls import path, include
from django.views.generic import TemplateView
from accounts.views import LoginView, RegisterView, LogoutView, guest_register_view
from addresses.views import checkout_address_create_view, checkout_address_reuse_view
from billing.views import create_payment_intent, payment_method_view, payment_success_view, payment_failed_view
from .views import (home_page,
about_page,
contact_page
)
urlpatterns = [
path('', home_page, name='home'),
path('about/', about_page, name='about'),
path('contact/', contact_page, name='contact'),
path('cart/', include("carts.urls", namespace="cart")),
path('checkout/address/create/', checkout_address_create_view, name='checkout_address_create'),
path('checkout/address/reuse/', checkout_address_reuse_view, name='checkout_address_reuse'),
path('login/', LoginView.as_view(), name='login'), # Login da loja
path('register/guest/', guest_register_view, name='guest_register'),
path('logout/', LogoutView.as_view(), name='logout'),
path('register/', RegisterView.as_view(), name='register'),
path('create-payment-intent', create_payment_intent, name='create-payment-intent'),
path('billing/payment-method/', payment_method_view, name='billing-payment-method'),
path('billing/payment-success/', payment_success_view, name='payment-success'),
path('billing/payment-failed/', payment_failed_view, name='payment-failed'),
path('bootstrap/', TemplateView.as_view(template_name='bootstrap/example.html')),
path('search/', include("search.urls", namespace="search")),
path('products/', include("products.urls", namespace="products")),
path('admin/', admin.site.urls),
]
if settings.DEBUG:
urlpatterns = urlpatterns + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns = urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Explicação do Código – e_commerce/products/admin.py
- Foi adicionada a importação de
format_html
para renderizar HTML no admin, usada no preview de imagens. - A classe
ProductImageInline
foi criada para gerenciar imagens relacionadas aos produtos no admin, com pré-visualização de imagens (image_preview
) e a possibilidade de adicionar novos campos extras. - O decorador
@admin.register(Product)
substituiu o registro manual do modeloProduct
, deixando o código mais elegante. - A classe
ProductAdmin
recebeu melhorias, incluindo:- Integração com
ProductImageInline
para gerenciar imagens na página de edição. - Exibição de novas colunas na listagem do admin (
list_display
). - Filtros laterais para os campos
active
efeatured
(list_filter
). - Campos de pesquisa para
title
edescription
(search_fields
). - Preenchimento automático do campo
slug
com base no título do produto (prepopulated_fields
). - Definição de uma ordenação padrão para os produtos (
ordering
).
- Integração com
e_commerce/products/admin.py
<!-- Path: admin.py -->
from django.contrib import admin
from django.utils.html import format_html # Import necessário para o método format_html
from .models import Product, ProductImage
# Inline para gerenciar imagens com preview
class ProductImageInline(admin.TabularInline): # ou admin.StackedInline se preferir
model = ProductImage
extra = 1 # Número de campos vazios extras para adicionar
readonly_fields = ['image_preview'] # Adicionando campo de visualização da imagem
def image_preview(self, obj):
"""Gera o HTML para exibir o preview da imagem."""
if obj.image:
return format_html('<img src="{}" style="width: 150px; height: auto;" />', obj.image.url)
return "Nenhuma imagem disponível"
image_preview.short_description = "Preview" # Nome da coluna no admin
# Configuração do modelo Product no admin
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
inlines = [ProductImageInline]
list_display = ['title', 'price', 'stock', 'active'] # Colunas exibidas na listagem
list_filter = ['active', 'featured'] # Filtros no painel lateral
search_fields = ['title', 'description'] # Campos para pesquisa
prepopulated_fields = {"slug": ("title",)} # Preenchimento automático do campo slug
ordering = ['-timestamp'] # Ordenação padrão (mais recente primeiro)
Refatorando os Javascripts
Agora vamos refatorar os arquivos JavaScript, organizando-os de forma mais eficiente e separando melhor as responsabilidades de cada um. Isso vai ajudar a manter o código mais limpo e facilitar a manutenção.
A estrutura dos arquivos vai ficar assim:
static_local/ js/ csrf.ajax.js <-- Gerenciamento de CSRF ecommerce/ contact.js <-- Formulário de contato search.js <-- Funcionalidade de busca cart.js <-- Funcionalidades do carrinho stripe/ stripe-payment.js <-- Pagamentos com Stripe
Explicação do Código – e_commerce/static_local/js/ecommerce/cart.js
- O código utiliza jQuery para manipular formulários de adicionar/remover produtos no carrinho de forma dinâmica, sem recarregar a página.
- Ele intercepta o envio dos formulários e realiza requisições AJAX com base nos dados do formulário enviado.
- Quando a requisição é bem-sucedida, o número de itens no carrinho exibido no navbar é atualizado.
- Atualiza o subtotal e o total do carrinho exibidos na página.
- Verifica se a quantidade do produto foi definida como 0. Nesse caso, remove a linha correspondente da tabela do carrinho e verifica se o carrinho está vazio.
- Inclui a função
refreshCart
que atualiza dinamicamente os itens do carrinho e suas informações usando uma requisição AJAX para obter os dados mais recentes. - Adiciona a função
checkCartEmpty
para verificar se o carrinho está vazio e recarregar a página nesse caso. - Trata erros de requisição com mensagens de alerta para o usuário e logs no console.
e_commerce/static_local/js/ecommerce/cart.js
$(document).ready(function () {
// Intercepta os formulários de adicionar/remover produtos
const productForm = $(".form-product-ajax");
productForm.submit(function (event) {
event.preventDefault();
const thisForm = $(this);
const actionEndpoint = thisForm.attr("action");
const httpMethod = thisForm.attr("method");
const formData = thisForm.serialize();
$.ajax({
url: actionEndpoint,
method: httpMethod,
data: formData,
success: function (data) {
if (data.success) {
// Atualiza o número de itens no carrinho no navbar
const navbarCount = $(".navbar-cart-count");
navbarCount.text(data.cartItemCount);
// Atualiza o subtotal e total na página
$(".cart-subtotal").text(`R$ ${data.subtotal}`);
$(".cart-total").text(`R$ ${data.total}`);
// Se estamos na página do carrinho
if (window.location.href.indexOf("cart") !== -1) {
// Se a quantidade for 0, remove a linha
if (thisForm.find('input[name="quantity"]').val() === "0") {
thisForm.closest("tr").fadeOut(300, function () {
$(this).remove();
checkCartEmpty(); // Verifica se o carrinho está vazio
});
} else {
refreshCart();
}
}
}
},
error: function () {
alert("Erro ao processar a solicitação. Tente novamente.");
},
});
});
// Função para atualizar a tabela do carrinho dinamicamente
function refreshCart() {
const cartTable = $(".cart-table");
const cartBody = cartTable.find("tbody");
const refreshCartUrl = "/cart/get-items/";
$.ajax({
url: refreshCartUrl,
method: "GET",
success: function (data) {
if (data.items.length > 0) {
cartBody.html("");
let i = data.items.length;
$.each(data.items, function (index, value) {
cartBody.append(`
<tr class="cart-product">
<th scope="row">${i}</th>
<td><a href="${value.url}">${value.name}</a></td>
<td>${value.quantity}</td>
<td>${value.price}</td>
<td>${value.total}</td>
<td>
<form method="POST" action="/cart/update/" class="form-product-ajax">
<input type="hidden" name="product_id" value="${value.id}">
<input type="number" name="quantity" value="0" class="d-none">
<button type="submit" class="btn btn-danger btn-sm">Remover</button>
</form>
</td>
</tr>
`);
i--;
});
$(".cart-subtotal").text(`R$ ${data.subtotal}`);
$(".cart-total").text(`R$ ${data.total}`);
} else {
// Recarrega a página para mostrar o carrinho vazio
window.location.href = window.location.href;
}
},
error: function () {
console.error("Erro ao atualizar o carrinho.");
},
});
}
// Verifica se o carrinho está vazio e atualiza a interface
function checkCartEmpty() {
if ($(".cart-product").length === 0) {
window.location.href = window.location.href; // Recarrega a página para mostrar o estado vazio
}
}
});
Explicação do Código – e_commerce/static_local/js/ecommerce/contact.js
Este código é responsável por gerenciar o envio do formulário de contato de maneira assíncrona, ou seja, sem precisar recarregar a página. Aqui está o que acontece:
- Manipulação do Formulário de Contato:
- O código começa pegando o formulário de contato (
.contact-form
) e configurando a URL de envio (action
) e o método HTTP (method
), usando esses dados para enviar a requisição AJAX posteriormente.
- O código começa pegando o formulário de contato (
- Exibição de “Enviando…” no botão de envio:
- A função
displaySubmitting()
é usada para mostrar que o formulário está sendo enviado. Quando o botão é clicado, ele é desabilitado e aparece o ícone de carregamento (“fa-spinner”). Quando o envio termina, o botão volta ao normal.
- A função
- Envio do Formulário (AJAX):
- Quando o formulário é enviado, a requisição AJAX é feita para o servidor. O método e a URL do formulário são usados para determinar como enviar os dados.
- O
contactForm.serialize()
pega todos os dados do formulário e envia para o servidor.
- Tratamento de Sucesso:
- Se o envio for bem-sucedido, o formulário é resetado e uma mensagem de sucesso é exibida usando a biblioteca
$.alert
. O botão de envio também retorna ao seu estado original após um pequeno delay.
- Se o envio for bem-sucedido, o formulário é resetado e uma mensagem de sucesso é exibida usando a biblioteca
- Tratamento de Erro:
- Caso ocorra algum erro, a mensagem de erro (seja do servidor ou de validação) é capturada e exibida em uma caixa de alerta. O botão de envio também retorna ao estado original após um pequeno delay.
e_commerce/static_local/js/ecommerce/contact.js
$(document).ready(function () {
// Contact Form Handler
const contactForm = $(".contact-form");
const contactFormMethod = contactForm.attr("method");
const contactFormEndpoint = contactForm.attr("action");
function displaySubmitting(submitBtn, defaultText, doSubmit) {
if (doSubmit) {
submitBtn.addClass("disabled");
submitBtn.html("<i class='fa fa-spin fa-spinner'></i> Enviando...");
} else {
submitBtn.removeClass("disabled");
submitBtn.html(defaultText);
}
}
contactForm.submit(function (event) {
event.preventDefault();
const contactFormSubmitBtn = contactForm.find("[type='submit']");
const contactFormSubmitBtnTxt = contactFormSubmitBtn.text();
const contactFormData = contactForm.serialize();
displaySubmitting(contactFormSubmitBtn, "", true);
$.ajax({
method: contactFormMethod,
url: contactFormEndpoint,
data: contactFormData,
success: function (data) {
contactForm[0].reset();
$.alert({
title: "Success!",
content: data.message,
theme: "modern",
});
setTimeout(function () {
displaySubmitting(
contactFormSubmitBtn,
contactFormSubmitBtnTxt,
false
);
}, 500);
},
error: function (error) {
const jsonData = error.responseJSON;
let msg = "";
$.each(jsonData, function (key, value) {
msg += `${key}: ${value[0].message}<br/>`;
});
$.alert({
title: "Oops!",
content: msg,
theme: "modern",
});
setTimeout(function () {
displaySubmitting(
contactFormSubmitBtn,
contactFormSubmitBtnTxt,
false
);
}, 500);
},
});
});
});
Explicação do Código – e_commerce/static_local/js/ecommerce/search.js
- Configuração inicial com
$(document).ready
: Aguarda o carregamento do documento para iniciar a execução do script. - Seleção do formulário e campo de busca:
searchForm
: Seleciona o formulário de busca usando a classe CSS.search-form
.searchInput
: Seleciona o campo de entrada de texto com o atributoname='q'
.
- Timer para digitação:
- Variável
typingTimer
usada para gerenciar atrasos entre as teclas pressionadas. typingInterval
define o intervalo de 0,5 segundos antes de iniciar a busca após parar de digitar.
- Variável
- Botão de busca:
searchBtn
seleciona o botão de envio do formulário.
- Eventos de teclado no campo de busca:
keyup
: Quando a tecla é liberada, limpa o temporizador anterior e inicia um novo.keydown
: Quando uma tecla é pressionada, cancela o temporizador para evitar buscas redundantes.
- Função
displaySearching
:- Desabilita o botão de busca.
- Mostra um ícone de carregamento e a mensagem “Searching…” no botão.
- Função
performSearch
:- Exibe o estado de carregamento chamando
displaySearching
. - Obtém o valor do campo de busca.
- Após 1 segundo, redireciona o navegador para a URL de busca com o parâmetro
q
.
- Exibe o estado de carregamento chamando
Fluxo de trabalho
- Usuário começa a digitar no campo de busca.
- Após 0,5 segundos sem digitar, o temporizador chama a função
performSearch
. - O botão de busca é desabilitado e o ícone de carregamento aparece.
- Após 1 segundo, o navegador é redirecionado para a página de resultados com o termo buscado.
Objetivo do Código
Fornecer uma experiência de busca mais interativa e dinâmica, redirecionando o usuário automaticamente após um pequeno atraso, sem necessidade de pressionar explicitamente o botão de envio.
e_commerce/static_local/js/ecommerce/search.js
$(document).ready(function () {
const searchForm = $(".search-form");
const searchInput = searchForm.find("[name='q']"); // input name='q'
let typingTimer;
const typingInterval = 500; // 0.5 segundos
const searchBtn = searchForm.find("[type='submit']");
searchInput.keyup(function () {
// Tecla liberada
clearTimeout(typingTimer);
typingTimer = setTimeout(performSearch, typingInterval);
});
searchInput.keydown(function () {
// Tecla pressionada
clearTimeout(typingTimer);
});
function displaySearching() {
searchBtn.addClass("disabled");
searchBtn.html("<i class='fa fa-spin fa-spinner'></i> Searching...");
}
function performSearch() {
displaySearching();
const query = searchInput.val();
setTimeout(function () {
window.location.href = "/search/?q=" + query;
}, 1000);
}
});
Explicação do Código – e_commerce/static_local/js/stripe/stripe-payment.js
- Função principal
$(document).ready
: Aguarda até que o documento esteja carregado e pronto para execução. - Função
initializeStripe
: Inicializa o Stripe se o elemento com a chave pública estiver presente. - Validação da chave pública do Stripe: Verifica se o elemento HTML com a chave pública do Stripe existe e se a chave foi fornecida corretamente.
- Função
initialize
: Faz uma requisição para criar um Payment Intent no backend e monta o elemento de cartão do Stripe na página. - Função
getCartItems
: Realiza uma requisição síncrona para obter os itens do carrinho de compras do backend. - Função
getCookie
: Recupera o valor de um cookie específico, usado aqui para o token CSRF. - Manipulação do formulário de pagamento: Configura o evento de envio do formulário para processar o pagamento usando o Stripe.
- Confirmação do pagamento com
stripe.confirmCardPayment
: Envia os dados do cartão e confirma o pagamento, lidando com os diferentes status de resposta. - Função
showMessage
: Exibe mensagens ao usuário, indicando o status do pagamento, com tratamento para erros. - Função
setLoading
: Controla o estado de carregamento do botão de envio e mostra um spinner durante o processamento do pagamento. - Requisição para obter Payment Intent: Inicializa os elementos do Stripe e monta o cartão na página.
- Tratamento de erros e redirecionamento: Redireciona o usuário para páginas de sucesso ou falha no pagamento, dependendo do resultado.
- Verificação de inicialização do Stripe: Inicializa o Stripe automaticamente se o elemento correspondente for encontrado na página.
Obs: Esse arquivo (stripe-payment.js) na verdade não tá sendo usado, a lógica do pagamento foi movida para o arquivo billing/templates/billing/payment-method.html
para evitar o carregamento desnecessário do script em outras páginas. O script que antes estava no static_local/js/stripe/stripe-payment.js
agora é carregado diretamente na página de pagamento, mas o arquivo antigo será mantido como precaução, já que ele não está sendo utilizado atualmente.
e_commerce/static_local/js/stripe/stripe-payment.js
$(document).ready(function () {
console.log("Documento pronto");
function initializeStripe() {
const stripeKeyElement = $("#stripe-key");
if (stripeKeyElement.length === 0) {
console.error("Elemento stripe-key não encontrado");
return;
}
const publishKey = stripeKeyElement.data("publishKey");
if (!publishKey) {
console.error("Chave pública do Stripe não encontrada");
return;
}
const stripe = Stripe(publishKey);
let elements,
cardElement,
clientSecret = null;
function initialize() {
const items = getCartItems(); // Obtém os itens do carrinho
$.ajax({
url: "/create-payment-intent",
method: "POST",
contentType: "application/json",
headers: {
"X-CSRFToken": getCookie("csrftoken"),
},
data: JSON.stringify({ items: items }),
success: function (data) {
if (data.error) {
console.error("Falha ao criar o intent de pagamento:", data.error);
return;
}
clientSecret = data.clientSecret;
elements = stripe.elements();
cardElement = elements.create("card");
cardElement.mount("#payment-element");
console.log("Elemento do cartão montado");
},
error: function (error) {
console.error("Erro ao inicializar os elementos do Stripe:", error);
},
});
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function getCartItems() {
let items = [];
$.ajax({
url: "/cart/get-items/", // Certifique-se que a URL está correta
method: "GET",
async: false, // Garante que a requisição seja concluída antes de prosseguir
success: function (data) {
items = data.items;
},
error: function (xhr, status, error) {
console.error("Erro ao obter itens do carrinho:", error);
},
});
return items;
}
const paymentForm = $("#payment-form");
if (paymentForm.length > 0) {
paymentForm.on("submit", function (event) {
event.preventDefault();
if (!elements || !cardElement) {
showMessage(
"Elemento do cartão não inicializado corretamente.",
true
);
return;
}
setLoading(true);
stripe
.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
// Inclua os detalhes de faturamento se necessário
},
},
})
.then(function (result) {
if (result.error) {
showMessage(result.error.message, true);
setTimeout(function () {
window.location.href = "/billing/payment-failed/";
}, 3000);
} else if (
result.paymentIntent &&
result.paymentIntent.status === "succeeded"
) {
showMessage("Pagamento realizado com sucesso!", false);
setTimeout(function () {
window.location.href = "/billing/payment-success/";
}, 3000);
} else {
showMessage(
"Pagamento não foi bem-sucedido. Tente novamente.",
true
);
setTimeout(function () {
window.location.href = "/billing/payment-failed/";
}, 3000);
}
setLoading(false);
})
.catch(function (error) {
showMessage("Ocorreu um erro inesperado.", true);
setTimeout(function () {
window.location.href = "/billing/payment-failed/";
}, 3000);
setLoading(false);
});
});
} else {
console.error("Formulário de pagamento não encontrado");
}
function showMessage(messageText, isError) {
const messageContainer = $("#payment-message");
messageContainer.text(messageText).removeClass("hidden");
if (isError) {
messageContainer.addClass("error-message");
} else {
messageContainer.removeClass("error-message");
}
setTimeout(function () {
messageContainer.addClass("hidden").text("");
}, 4000);
}
function setLoading(isLoading) {
const submitButton = $("#submit");
submitButton.prop("disabled", isLoading);
$("#spinner").toggleClass("hidden", !isLoading);
$("#button-text").toggleClass("hidden", isLoading);
}
initialize();
}
// Inicializa o Stripe diretamente se o elemento estiver presente
const stripeKeyElement = $("#stripe-key");
if (stripeKeyElement.length > 0) {
initializeStripe();
}
});
Explicação do Código – e_commerce/static_local/js/toast.js
- O código começa com
document.addEventListener("DOMContentLoaded", function() {...})
, que aguarda até o conteúdo da página ser totalmente carregado antes de executar o código dentro da função. - Em seguida,
document.querySelectorAll('.toast')
seleciona todos os elementos na página com a classe.toast
(os elementos de notificação toast). - A expressão
[].slice.call(...)
converte a lista de elementos (que originalmente é um NodeList) em um array para poder usar métodos de array, comomap()
. - O
toastElList.map(function(toastEl) {...})
itera sobre cada item na lista e executa uma função que cria uma nova instância debootstrap.Toast
para cada elemento encontrado. - Finalmente,
new bootstrap.Toast(toastEl)
inicializa o toast para que ele funcione corretamente, permitindo que o comportamento padrão do Bootstrap seja aplicado (como aparecer e desaparecer automaticamente).
e_commerce/static_local/js/toast.js
document.addEventListener("DOMContentLoaded", function() {
var toastElList = [].slice.call(document.querySelectorAll('.toast'));
toastElList.map(function(toastEl) {
return new bootstrap.Toast(toastEl);
});
});
E pode deletar o e_commerce/static_local/js/ecommerce.js.
Logging
Explicação das Partes Novas – e_commerce/e_commerce/settings.py
- Configuração de variáveis de ambiente essenciais:
SECRET_KEY
,DEBUG
eALLOWED_HOSTS
são obtidas do arquivo.env
, garantindo que informações sensíveis e específicas do ambiente não estejam hardcoded no código. - Adição do aplicativo
categories
emINSTALLED_APPS
, indicando que um novo módulo foi incluído para gerenciar categorias no projeto. - Configurações detalhadas de sessões e cookies, incluindo a engine de sessão, nome do cookie, tempo de expiração, políticas de segurança (
SameSite
eHttpOnly
) e proteção CSRF. Essas configurações reforçam a segurança e o comportamento das sessões. - Definição de
CSRF_TRUSTED_ORIGINS
, permitindo solicitações confiáveis de URLs locais durante o desenvolvimento. - Redefinição de
LOGOUT_REDIRECT_URL
paraNone
, alterando o comportamento padrão ao fazer logout. - Configuração do banco de dados utilizando a variável
DATABASE_URL
do arquivo.env
, com fallback para o SQLite padrão. Isso permite flexibilidade para usar diferentes bancos de dados sem alterar o código. - Configuração de um sistema básico de logging, que inclui um handler para exibir mensagens de log no console, útil para depuração e monitoramento do sistema.
e_commerce/e_commerce/settings.py
import os
import environ
from django.contrib.messages import constants as messages
import logging
logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
env = environ.Env(DEBUG=(bool, False),)
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
SECRET_KEY = env('SECRET_KEY') # Sem valor padrão para forçar a configuração via .env
DEBUG = env.bool('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
STRIPE_API_KEY = env('STRIPE_API_KEY')
STRIPE_PUB_KEY = env('STRIPE_PUB_KEY')
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Apps customizados
'addresses',
'analytics',
'billing',
'accounts',
'carts',
'orders',
'products',
'search',
'tags',
'categories',
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = 'accounts.User'
FORCE_SESSION_TO_ONE = False
FORCE_INACTIVE_USER_ENDSESSION = False
# Configurações de Sessão e CSRF
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_NAME = 'sessionid'
SESSION_COOKIE_AGE = 1209600 # 2 semanas
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = False
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', # Proteção contra CSRF
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Configurações de Mensagens
MESSAGE_STORAGE = 'django.contrib.messages.storage.fallback.FallbackStorage'
MESSAGE_TAGS = {
messages.DEBUG: 'debug',
messages.INFO: 'info',
messages.SUCCESS: 'success',
messages.WARNING: 'warning',
messages.ERROR: 'error',
}
CSRF_TRUSTED_ORIGINS = [
'http://localhost:8000',
'http://127.0.0.1:8000',
]
LOGOUT_REDIRECT_URL = None
ROOT_URLCONF = 'e_commerce.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'e_commerce.wsgi.application'
DATABASES = {
'default': env.db('DATABASE_URL', default='sqlite:///db.sqlite3')
}
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static_local")]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "static_root")
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static_cdn", "media_root")
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {'level': 'DEBUG', 'class': 'logging.StreamHandler'},
},
'root': {'handlers': ['console'], 'level': 'DEBUG'},
}
Explicação do Código – django_ecommerce/e_commerce/accounts/forms.py
- Define os campos visíveis no formulário, incluindo “email” e “full_name”.
- Cria o usuário, define a senha em formato hash e ajusta o campo
is_verified
comoFalse
antes de salvar. - Define os campos visíveis no formulário de atualização de usuários, incluindo “full_name”, “email”, “password”, “active”, “admin” e “is_verified”.
- Adiciona atributos CSS (
class="form-control"
) aos campos “email” e “password” para estilização do formulário. - Inclui “full_name” no formulário de registro, permitindo capturar o nome completo do usuário.
- Configura o campo
is_verified
paraFalse
por padrão ao criar um novo usuário.
django_ecommerce/e_commerce/accounts/forms.py
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import ReadOnlyPasswordHashField
User = get_user_model()
class UserAdminCreationForm(forms.ModelForm):
"""
A form for creating new users. Includes all the required
fields, plus a repeated password.
"""
password = forms.CharField(widget=forms.PasswordInput)
password_2 = forms.CharField(label='Confirm Password', widget=forms.PasswordInput)
class Meta:
model = User
fields = ['email', 'full_name']
def clean(self):
'''
Verify both passwords match.
'''
cleaned_data = super().clean()
password = cleaned_data.get("password")
password_2 = cleaned_data.get("password_2")
if password is not None and password != password_2:
self.add_error("password_2", "Your passwords must match")
return cleaned_data
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserAdminCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password"])
user.is_verified = False
if commit:
user.save()
return user
class UserAdminChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta:
model = User
fields = ['full_name', 'email', 'password', 'active', 'admin', 'is_verified']
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
class GuestForm(forms.Form):
email = forms.EmailField()
class LoginForm(forms.Form):
email = forms.EmailField(label="Email", max_length=255, widget=forms.EmailInput(attrs={"class": "form-control"}))
password = forms.CharField(label="Senha", max_length=255, widget=forms.PasswordInput(attrs={"class": "form-control"}))
class RegisterForm(forms.ModelForm):
"""
A form for creating new users. Includes all the required
fields, plus a repeated password.
"""
password = forms.CharField(widget=forms.PasswordInput)
password_2 = forms.CharField(label='Confirm Password', widget=forms.PasswordInput)
class Meta:
model = User
fields = ['email', 'full_name'] # [ADDED] Incluído full_name no formulário
def clean(self):
'''
Verify both passwords match.
'''
cleaned_data = super().clean()
password = cleaned_data.get("password")
password_2 = cleaned_data.get("password_2")
if password is not None and password != password_2:
self.add_error("password_2", "Your passwords must match")
return cleaned_data
def save(self, commit=True):
# Save the provided password in hashed format
user = super(RegisterForm, self).save(commit=False)
user.set_password(self.cleaned_data["password"])
user.is_verified = False # [ADDED] Usuários não verificados por padrão
if commit:
user.save()
return user
Explicação do Código – e_commerce/accounts/models.py
- Adição de logging: Implementado o uso de logs para registrar erros e sucessos nos processos de criação de usuários, facilitando o monitoramento e a depuração.
- Campo
is_verified
: Adicionado ao modeloUser
para indicar se o email do usuário foi verificado. Incluído também nos métodos de criação de usuários (create_user
,create_staffuser
, ecreate_superuser
) com valor padrão comoFalse
. - Ajustes nos métodos de criação: Os métodos de criação (
create_user
,create_staffuser
, ecreate_superuser
) foram atualizados para incluir o campois_verified
e registrar mensagens de log em cada etapa.
Essas mudanças aprimoram o controle sobre usuários verificados e adicionam rastreabilidade por meio de logs.
e_commerce/accounts/models.py
from django.db import models
import logging
logger = logging.getLogger('accounts') # Logger configurado para o app accounts
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager
)
# Gerenciador de usuários
class UserManager(BaseUserManager):
def create_user(self, email, full_name=None, password=None, is_active=True, is_staff=False, is_admin=False, is_verified=False):
"""
Cria e retorna um usuário regular.
"""
if not email:
logger.error("Falha ao criar usuário: E-mail é obrigatório.")
raise ValueError("O Usuário deve ter um endereço de email!")
if not password:
logger.error("Falha ao criar usuário: senha necessária.")
raise ValueError("O Usuário deve ter uma senha!")
user_obj = self.model(
full_name=full_name,
email=self.normalize_email(email),
)
user_obj.set_password(password) # Configura a senha
user_obj.staff = is_staff
user_obj.admin = is_admin
user_obj.active = is_active
user_obj.is_verified = is_verified
user_obj.save(using=self._db)
logger.info(f"Usuário criado com sucesso: {email}")
return user_obj
def create_staffuser(self, email, full_name=None, password=None):
"""
Cria e retorna um usuário da equipe.
"""
logger.info(f"Tentando criar um usuário de equipe: {email}")
user = self.create_user(
email=email,
full_name=full_name,
password=password,
is_active=True,
is_staff=True,
is_admin=False,
is_verified=False
)
logger.info(f"Usuário da equipe criado com sucesso: {email}")
return user
def create_superuser(self, email, full_name=None, password=None):
"""
Cria e retorna um superusuário.
"""
logger.info(f"Tentando criar superusuário: {email}")
user = self.create_user(
email=email,
full_name=full_name,
password=password,
is_active=True,
is_staff=True,
is_admin=True,
is_verified=True
)
logger.info(f"Superusuário criado com sucesso: {email}")
return user
# Modelo de usuário personalizado
class User(AbstractBaseUser):
full_name = models.CharField(max_length=255, blank=True, null=True)
email = models.EmailField(max_length=255, unique=True)
active = models.BooleanField(default=True) # Pode fazer login
staff = models.BooleanField(default=False) # Usuário da equipe, não superusuário
admin = models.BooleanField(default=False) # Superusuário
is_verified = models.BooleanField(default=False, help_text="Indica se o email do usuário foi verificado.")
timestamp = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email' # Campo principal para login
REQUIRED_FIELDS = [] # Campos obrigatórios além do email e senha
objects = UserManager()
def __str__(self):
return self.email
def get_full_name(self):
return self.full_name or self.email
def get_short_name(self):
return self.email
def has_module_perms(self, app_label):
return True
def has_perm(self, perm, obj=None):
return True
@property
def is_staff(self):
return self.staff
@property
def is_admin(self):
return self.admin
@property
def is_active(self):
return self.active
# Modelo para e-mails de convidados
class GuestEmail(models.Model):
email = models.EmailField()
active = models.BooleanField(default=True)
update = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.email
Explicação do Código – e_commerce/accounts/views.py
- Importa o módulo
messages
do Django, usado para exibir mensagens temporárias de feedback ao usuário. - Utiliza a função
reverse
para gerar URLs dinamicamente com base no nome de referência das rotas, garantindo flexibilidade em alterações futuras. - Verifica se existe um caminho de redirecionamento (
redirect_path
) e redireciona o usuário para ele, ou, se não existir, redireciona para a página de registro de contas (accounts:register
). - No método
form_valid
daLoginView
, autentica o usuário, faz login e envia um sinal personalizadouser_logged_in
. Remove qualquerguest_email_id
da sessão e redireciona parasuccess_url
. - Exibe uma mensagem de erro com
messages.error
caso as credenciais de login sejam inválidas. - No método
get
daLogoutView
, realiza o logout do usuário e redireciona para a página de login ou raiz do site, dependendo do estado de autenticação. - Define
success_url
naRegisterView
para redirecionar para a página de login após o registro bem-sucedido. - No método
form_valid
daRegisterView
, salva o usuário com a senha definida e exibe uma mensagem de sucesso commessages.success
após o registro. - No método
form_invalid
daRegisterView
, exibe mensagens de erro detalhadas para cada campo do formulário usandomessages.error
.
e_commerce/accounts/views.py
from django.contrib.auth import authenticate, login, logout
from django.views.generic import CreateView, FormView, View
from django.http import HttpResponse
from django.shortcuts import redirect
from .signals import user_logged_in
from django.contrib import messages
from .forms import LoginForm, RegisterForm, GuestForm
from .models import GuestEmail
from django.shortcuts import redirect
from django.urls import reverse
def guest_register_view(request):
form = GuestForm(request.POST or None)
context = {
"form": form
}
next_ = request.GET.get('next')
next_post = request.POST.get('next')
redirect_path = next_ or next_post or None
if form.is_valid():
email = form.cleaned_data.get("email")
new_guest_email = GuestEmail.objects.create(email=email)
request.session['guest_email_id'] = new_guest_email.id
if redirect_path:
return redirect(redirect_path)
else:
return redirect(reverse("accounts:register"))
return redirect(reverse("accounts:register"))
class LoginView(FormView):
form_class = LoginForm
success_url = '/' # Redireciona para a raiz do projeto
template_name = 'accounts/login.html'
def form_valid(self, form):
email = form.cleaned_data.get("email")
password = form.cleaned_data.get("password")
user = authenticate(request=self.request, username=email, password=password)
if user is not None:
login(self.request, user)
user_logged_in.send(sender=user.__class__, instance=user, request=self.request)
try:
del self.request.session['guest_email_id']
except:
pass
return redirect(self.success_url)
else:
# Mensagem de erro ao usuário
messages.error(self.request, "Email ou senha inválidos. Tente novamente.")
return self.form_invalid(form) # Retorna o formulário com erro
class LogoutView(View):
"""
View de logout com redirecionamento customizado.
"""
def get(self, request, *args, **kwargs):
logout(request)
if request.user.is_authenticated:
return redirect('/accounts/login/')
return redirect('/')
class RegisterView(CreateView):
form_class = RegisterForm
template_name = 'accounts/register.html'
success_url = '/accounts/login/'
def form_valid(self, form):
user = form.save(commit=False)
user.set_password(form.cleaned_data["password"])
user.save()
# Usando messages.success diretamente
messages.success(self.request, "Usuário registrado com sucesso! Agora você pode fazer login.")
return super().form_valid(form)
def form_invalid(self, form):
# Apenas um método form_invalid
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field.capitalize()}: {error}")
return super().form_invalid(form)
Explicação do Código – django_ecommerce/e_commerce/accounts/urls.py
- Define o namespace do aplicativo como
'accounts'
, utilizado para organizar as URLs no projeto Django. - Cria a URL
/login/
, associada àLoginView
, utilizando o nome de referência'login'
. - Cria a URL
/register/
, associada àRegisterView
, utilizando o nome de referência'register'
. - Cria a URL
/guest_register/
, associada à funçãoguest_register_view
, utilizando o nome de referência'guest_register'
. - Cria a URL
/logout/
, associada àLogoutView
, utilizando o nome de referência'logout'
.
django_ecommerce/e_commerce/accounts/urls.py
from django.urls import path
from .views import LoginView, RegisterView, guest_register_view, LogoutView
app_name = 'accounts'
urlpatterns = [
path('login/', LoginView.as_view(), name='login'),
path('register/', RegisterView.as_view(), name='register'),
path('guest_register/', guest_register_view, name='guest_register'),
path('logout/', LogoutView.as_view(), name='logout'),
]
Explicação do código – e_commerce/products/models.py
- Importação de
Category
:
Foi adicionada a importação do modeloCategory
, permitindo associar produtos a categorias, melhorando a organização e filtragem de produtos. - Ajuste no
search
doQuerySet
:
A pesquisa foi aprimorada para ser insensível a maiúsculas e minúsculas, tornando a busca mais flexível e amigável para o usuário. - Novos campos no modelo
Product
:discount_price
: Permite registrar um preço com desconto, oferecendo mais flexibilidade na precificação.stock
: Adiciona controle de estoque, essencial para verificar a disponibilidade do produto.category
: Relaciona o produto a uma categoria, utilizando uma chave estrangeira com exclusão opcional (SET_NULL
).sku
: Adicionado para identificar de forma única os produtos no sistema.updated
: Rastreia quando o produto foi atualizado pela última vez.
- Novos métodos no modelo
Product
:get_final_price
: Calcula o preço final considerando o preço com desconto, se aplicável.has_stock
: Verifica se o produto tem estoque disponível, útil para validações e exibição no frontend.
- Adição do modelo
ProductImage
:- Introduz um modelo para gerenciar imagens de produtos, com suporte a múltiplas imagens para cada produto.
- Campos adicionais incluem
alt_text
(texto alternativo para SEO),is_featured
(destacar imagem) eorder
(ordem de exibição). - Permite associar imagens a produtos com um relacionamento
ForeignKey
.
Essas mudanças introduzem recursos essenciais, como controle de estoque, organização por categorias, suporte a múltiplas imagens e precificação avançada, elevando a funcionalidade do sistema de gerenciamento de produtos.
e_commerce/products/models.py
from django.db.models import Q
from django.db import models
from e_commerce.utils import unique_slug_generator
from django.db.models.signals import pre_save
from django.urls import reverse
from categories.models import Category # Importando Category
# Custom queryset
class ProductQuerySet(models.query.QuerySet):
def active(self):
return self.filter(active=True)
def featured(self):
return self.filter(featured=True, active=True)
def search(self, query):
lookups = (
Q(title__icontains=query) |
Q(description__icontains=query) |
Q(price__icontains=query)
)
return self.filter(lookups).distinct()
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def all(self):
return self.get_queryset().active()
def featured(self):
return self.get_queryset().featured()
def get_by_id(self, id):
qs = self.get_queryset().filter(id=id)
if qs.count() == 1:
return qs.first()
return None
def search(self, query):
return self.get_queryset().active().search(query)
# Product model
class Product(models.Model): # product_category
title = models.CharField(max_length=120)
slug = models.SlugField(blank=True, unique=True)
description = models.TextField()
price = models.DecimalField(decimal_places=2, max_digits=20, default=100.00)
discount_price = models.DecimalField(decimal_places=2, max_digits=20, null=True, blank=True)
stock = models.PositiveIntegerField(default=0)
category = models.ForeignKey(Category, null=True, blank=True, on_delete=models.SET_NULL, related_name="products")
sku = models.CharField(max_length=20, unique=True, null=True, blank=True)
featured = models.BooleanField(default=False)
updated = models.DateTimeField(auto_now=True)
active = models.BooleanField(default=True)
timestamp = models.DateTimeField(auto_now_add=True)
objects = ProductManager()
def get_absolute_url(self):
return reverse("products:detail", kwargs={"slug": self.slug})
def get_final_price(self):
"""Retorna o preço com desconto, se disponível."""
if self.discount_price:
return self.discount_price
return self.price
def has_stock(self):
"""Verifica se o produto tem estoque."""
return self.stock > 0
def __str__(self):
return self.title
def product_pre_save_receiver(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = unique_slug_generator(instance)
pre_save.connect(product_pre_save_receiver, sender=Product)
# ProductImage model
class ProductImage(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="images")
image = models.ImageField(upload_to='products/')
alt_text = models.CharField(max_length=255, null=True, blank=True) # Texto alternativo para SEO
is_featured = models.BooleanField(default=False)
order = models.PositiveIntegerField(default=0)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Image for {self.product.title}"
Explicação do Código – /products/templates/products/snippets/card.html
- Verificação de múltiplas imagens: Agora o código exibe a primeira imagem relacionada ao produto usando
instance.images.first.image.url
. Se o produto não tiver imagens, exibe uma imagem de “placeholder” (static 'placeholder.jpg'
). - Badge de promoção: Adicionado um destaque visual com a palavra “Promoção” caso o produto tenha um preço com desconto (
instance.discount_price
). - Preços com desconto: Exibido o preço original riscado e o preço com desconto em vermelho e negrito. Se não houver desconto, exibe apenas o preço normal.
- Campo de quantidade no formulário: Um campo numérico foi incluído para que o usuário escolha a quantidade do produto antes de adicioná-lo ao carrinho. O valor padrão é
1
, e o máximo permitido é o estoque disponível (instance.stock
). - Botão “Adicionar” estilizado: O botão foi estilizado para ser menor (
btn-sm
) e posicionado ao lado do campo de quantidade, melhorando a usabilidade e o design.
Essas mudanças tornam o design mais moderno, funcional e orientado à experiência do usuário.
e_commerce/products/templates/products/snippets/card.html
<!-- Path: templates/products/snippets/card.html -->
{% load static %}
<div class="card h-100 shadow-sm">
<div class="card-img-wrapper position-relative">
{% if instance.images.all %}
<a href="{{ instance.get_absolute_url }}">
<img src="{{ instance.images.first.image.url }}" class="card-img-top rounded" alt="{{ instance.images.first.alt_text|default:instance.title }}">
</a>
{% else %}
<img src="{% static 'placeholder.jpg' %}" class="card-img-top rounded" alt="Placeholder">
{% endif %}
{% if instance.discount_price %}
<span class="badge bg-danger position-absolute top-0 start-0 m-2">Promoção</span>
{% endif %}
</div>
<div class="card-body d-flex flex-column">
<h5 class="card-title text-center text-uppercase fw-bold">{{ instance.title }}</h5>
<p class="card-text text-muted text-center flex-grow-1">{{ instance.description|truncatewords:14 }}</p>
{% if instance.discount_price %}
<p class="card-text text-center">
<span class="text-muted text-decoration-line-through">R$ {{ instance.price }}</span>
<span class="text-danger fw-bold">R$ {{ instance.discount_price }}</span>
</p>
{% else %}
<p class="card-text text-center fw-bold">R$ {{ instance.price }}</p>
{% endif %}
</div>
<div class="card-footer bg-light border-0">
<form
method="POST"
action="{% url 'cart:update' %}"
class="d-flex align-items-center justify-content-between form-product-ajax"
>
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ instance.id }}">
<!-- Campo de Quantidade -->
<div class="me-2">
<input
type="number"
name="quantity"
value="1"
min="1"
max="{{ instance.stock }}"
class="form-control form-control-sm text-center"
style="width: 60px;"
>
</div>
<!-- Botão Adicionar -->
<button
type="submit"
class="btn btn-primary btn-sm"
style="padding: 0.375rem 0.75rem; font-size: 14px;"
>
Adicionar
</button>
</form>
</div>
</div>
Explicação do Código – products/templates/products/detail.html
e_commerce/products/templates/products/detail.html
<!-- Path: templates/products/detail.html -->
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="row">
<div class="col-12 col-md-6">
{% if object.images.all %}
<div id="productCarousel" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner">
{% for image in object.images.all %}
<div class="carousel-item {% if forloop.first %}active{% endif %}">
<img src="{{ image.image.url }}" class="d-block w-100" alt="{{ image.alt_text|default:object.title }}">
</div>
{% endfor %}
</div>
{% if object.images.count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#productCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#productCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
{% endif %}
</div>
{% endif %}
</div>
<div class="col-12 col-md-6">
<h1>{{ object.title }}</h1>
{% if object.discount_price %}
<h3 class="mb-3">
<span class="text-muted text-decoration-line-through">R$ {{ object.price }}</span>
<span class="text-danger">R$ {{ object.discount_price }}</span>
</h3>
{% else %}
<h3 class="mb-3">R$ {{ object.price }}</h3>
{% endif %}
<div class="mb-4">
<p>{{ object.description|linebreaks }}</p>
</div>
{% if object.stock > 0 %}
<p class="text-success">Em estoque: {{ object.stock }} unidades</p>
{% else %}
<p class="text-danger">Produto indisponível</p>
{% endif %}
{% if object.category %}
<p class="mb-3">Categoria: <a href="{% url 'products:category' object.category.slug %}">{{ object.category.title }}</a></p>
{% endif %}
{% include 'products/snippets/update-cart.html' with product=object %}
</div>
</div>
</div>
{% endblock content %}
Explicação do Código – products/templates/products/featured-detail.html
- Suporte a múltiplas imagens:
Antes, o código verificava apenas uma única imagem usandoobject.image
e exibia essa imagem, se presente. Agora, ele verifica se há múltiplas imagens relacionadas ao produto comobject.images.all
e usa um loopfor
para exibir todas as imagens associadas. Isso melhora a apresentação do produto ao permitir que o usuário veja várias imagens. - Adição do atributo
alt
:
Cada imagem agora inclui um atributoalt
para acessibilidade e SEO. Ele utiliza o campoalt_text
da imagem, e caso não esteja definido, usa o título do produto (object.title
) como valor padrão. Isso torna a página mais acessível para tecnologias assistivas e melhora a indexação nos mecanismos de busca.
Essas alterações tornam o sistema mais robusto e amigável para os usuários, com suporte aprimorado para produtos que têm múltiplas imagens.
e_commerce/products/templates/products/featured-detail.html
{{ object.title }}
{{ object.description }}
{% if object.images.all %}
{% for image in object.images.all %}
<img src="{{ image.image.url }}" class="img-fluid" alt="{{ image.alt_text|default:object.title }}">
{% endfor %}
{% endif %}
Explicação do Código – e_commerce/products/templates/products/snippets/update-cart.html
- Adição do campo de quantidade:
Agora, os usuários podem especificar diretamente a quantidade de produtos a ser adicionada ao carrinho. O campo tem um valor inicial de1
e é limitado ao estoque disponível do produto (max="{{ product.stock }}"
). Isso melhora a usabilidade ao permitir compras em maiores quantidades. - Botão fixo “Adicionar”:
O botão “Adicionar” foi simplificado e não muda mais entre “Adicionar” e “Remover” com base na presença do produto no carrinho. Essa simplificação elimina lógica desnecessária no frontend e deixa a remoção de produtos para outra funcionalidade. - Remoção da verificação de produtos no carrinho:
O código que verificava se o produto já estava no carrinho ({% if product in cart.products.all %}
) foi removido. Isso reduz a complexidade do formulário e melhora a performance. - Reorganização do layout:
O campo de quantidade e o botão foram organizados dentro de um contêiner flexível (d-flex align-items-center
) para melhorar a responsividade e alinhamento. Isso cria uma interface visualmente mais limpa e funcional.
Essas mudanças simplificam a interface, tornam o processo de adicionar produtos ao carrinho mais direto e melhoram a experiência do usuário ao permitir maior controle sobre as quantidades compradas.
e_commerce/products/templates/products/snippets/update-cart.html
<!-- Path: templates/products/snippets/update-cart.html -->
<form class="form-product-ajax" method="POST" action="{% url 'cart:update' %}">
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ product.id }}">
<div class="row g-2 align-items-center">
<div class="col-auto">
<input
type="number"
name="quantity"
value="1"
min="1"
max="{{ product.stock }}"
class="form-control form-control-sm text-center"
style="width: 80px;"
>
</div>
<div class="col-auto">
<button
type="submit"
class="btn btn-primary btn-sm"
>
Adicionar
</button>
</div>
</div>
</form>
Explicação do Código – e_commerce/carts/templates/carts/home.html
- O template é responsável por exibir os detalhes do carrinho de compras de um usuário.
- Ele verifica se existem produtos no carrinho usando a condição
cart.cartproduct_set.exists
. - Se houver produtos, exibe uma tabela que lista os itens do carrinho com colunas para número, nome do produto, imagem, quantidade, preço unitário, total e ações.
- Para cada produto, é exibida a imagem principal ou um placeholder caso não haja imagens associadas ao produto.
- A quantidade do produto pode ser ajustada com um campo de entrada numérica que envia uma requisição ao servidor via um formulário
POST
comcsrf_token
para segurança. - Os preços são formatados para exibir o preço original e o preço com desconto, caso exista, destacando o desconto visualmente.
- Inclui um botão para remover o item do carrinho, enviando a quantidade como zero no formulário
POST
. - Exibe o subtotal e o total do carrinho, calculados dinamicamente no backend.
- Caso o carrinho esteja vazio, exibe uma mensagem informando o usuário e um botão para continuar comprando.
- A estrutura utiliza classes CSS do Bootstrap para estilização responsiva e visual atraente.
e_commerce/carts/templates/carts/home.html
{% extends "base.html" %}
{% load static %}
{% load custom_filters %}
{% block content %}
<h1>Carrinho</h1>
{% if cart.cartproduct_set.exists %}
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Produto</th>
<th>Imagem</th>
<th>Quantidade</th>
<th>Preço Unitário</th>
<th>Total</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{% for cart_product in cart.cartproduct_set.all %}
<tr class="align-middle">
<td>{{ forloop.counter }}</td>
<td>{{ cart_product.product.title }}</td>
<td>
{% if cart_product.product.images.all %}
<img
src="{{ cart_product.product.images.first.image.url }}"
alt="{{ cart_product.product.images.first.alt_text|default:cart_product.product.title }}"
class="img-thumbnail"
style="width: 100px; height: auto;"
>
{% else %}
<img
src="{% static 'placeholder.jpg' %}"
alt="Placeholder"
class="img-thumbnail"
style="width: 100px; height: auto;"
>
{% endif %}
</td>
<td>
<form method="POST" action="{% url 'cart:update' %}" class="form-product-ajax">
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ cart_product.product.id }}">
<input
type="number"
name="quantity"
value="{{ cart_product.quantity }}"
min="1"
max="{{ cart_product.product.stock }}"
class="form-control form-control-sm text-center"
onchange="this.form.submit()"
style="width: 70px;"
>
</form>
</td>
<td>
{% if cart_product.product.discount_price %}
<div>
<span class="text-muted text-decoration-line-through">R$ {{ cart_product.product.price }}</span>
</div>
<div>
<span class="text-danger fw-bold">R$ {{ cart_product.product.discount_price }}</span>
</div>
{% else %}
<span class="fw-bold">R$ {{ cart_product.product.get_final_price }}</span>
{% endif %}
</td>
<td class="fw-bold">R$ {{ cart_product.quantity|multiply:cart_product.product.get_final_price }}</td>
<td>
<form method="POST" action="{% url 'cart:update' %}" class="form-product-ajax">
{% csrf_token %}
<input type="hidden" name="product_id" value="{{ cart_product.product.id }}">
<input type="hidden" name="quantity" value="0">
<button
type="submit"
class="btn btn-danger btn-sm"
>
Remover
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row mt-4">
<div class="col-12 col-md-6 offset-md-6">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>Subtotal:</span>
<span class="cart-subtotal fw-bold">R$ {{ cart.subtotal }}</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span>Total:</span>
<span class="cart-total fw-bold fs-5">R$ {{ cart.total }}</span>
</div>
<div class="d-grid">
<a href="{% url 'carts:checkout' %}" class="btn btn-primary">Finalizar Compra</a>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-info shadow-sm">
<p class="mb-3">Seu carrinho está vazio!</p>
<a href="{% url 'home' %}" class="btn btn-primary">Continuar Comprando</a>
</div>
{% endif %}
{% endblock content %}
Explicação do Código – e_commerce/carts/templates/carts/snippets/checkout.html
sdff
e_commerce/carts/templates/carts/snippets/checkout.html
<{% extends "base.html" %}>
<{% load static %}>
<{% load custom_filters %}>
<{% block content %}>
<div class="container mt-5">
<{% if not billing_profile %}>
<div class="row g-4 justify-content-center">
<!-- Formulário de Login -->
<div class="col-12 col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4 text-primary">Login</h3>
<{% include 'accounts/snippets/form.html' with form=login_form next_url=request.build_absolute_uri %}>
</div>
</div>
</div>
<!-- Formulário de Convidado -->
<div class="col-12 col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4 text-primary">Continuar como Convidado</h3>
<{% url "guest_register" as guest_register_url %}>
<{% include 'accounts/snippets/form.html' with form=guest_form next_url=request.build_absolute_uri action_url=guest_register_url %}>
</div>
</div>
</div>
</div>
<{% else %}>
<{% if not object.shipping_address %}>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4 text-primary">Endereço de Envio</h3>
<form method="POST" action="{% url 'checkout_address_create' %}">
<{% csrf_token %}>
<input type="hidden" name="address_type" value="billing">
<{% for field in address_form %}>
<div class="form-group mb-3">
<label for="{{ field.id_for_label }}" class="form-label fw-bold">{{ field.label }}</label>
{{ field|add_class:"form-control form-control-lg" }}
<{% if field.help_text %}>
<small class="form-text text-muted">{{ field.help_text }}</small>
<{% endif %}>
</div>
<{% endfor %}>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary btn-lg">Salvar Endereço</button>
<a href="{% url 'cart:home' %}" class="btn btn-outline-secondary btn-lg">Voltar ao Carrinho</a>
</div>
</form>
</div>
</div>
</div>
</div>
<{% elif not object.billing_address %}>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4 text-primary">Endereço de Cobrança</h3>
<form method="POST" action="{% url 'checkout_address_create' %}">
<{% csrf_token %}>
<input type="hidden" name="next" value="{{ request.build_absolute_uri }}">
<input type="hidden" name="address_type" value="billing">
<{% for field in address_form %}>
<div class="form-group mb-3">
<label for="{{ field.id_for_label }}" class="form-label fw-bold">{{ field.label }}</label>
{{ field|add_class:"form-control form-control-lg" }}
<{% if field.help_text %}>
<small class="form-text text-muted">{{ field.help_text }}</small>
<{% endif %}>
</div>
<{% endfor %}>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary btn-lg">Salvar Endereço</button>
<a href="{% url 'cart:home' %}" class="btn btn-outline-secondary btn-lg">Voltar ao Carrinho</a>
</div>
</form>
</div>
</div>
</div>
</div>
<{% else %}>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4 text-primary">Resumo do Pedido</h3>
<ul class="list-group list-group-flush">
<li class="list-group-item"><strong>Itens do Carrinho:</strong> {{ object.cart.products.all|join:", " }}</li>
<li class="list-group-item"><strong>Envio:</strong> {{ object.shipping_address.get_address }}</li>
<li class="list-group-item"><strong>Cobrança:</strong> {{ object.billing_address.get_address }}</li>
<li class="list-group-item"><strong>Total:</strong> R$ {{ object.total }}</li>
</ul>
<div class="d-grid gap-2 mt-4">
<form method="POST">
<{% csrf_token %}>
<button type="submit" class="btn btn-success btn-lg">Finalizar Pedido</button>
</form>
<a href="{% url 'cart:home' %}" class="btn btn-outline-secondary btn-lg">Voltar ao Carrinho</a>
</div>
</div>
</div>
</div>
</div>
<{% endif %}>
<{% endif %}>
</div>
<{% endblock content %}>
Explicação do Código – e_commerce/billing/templates/billing/payment-method.html
- O formulário está envolvido em uma estrutura Bootstrap, com classes como
container mt-5
para espaçamento superior erow justify-content-center
para centralização horizontal do conteúdo. A classecol-md-6
define que o formulário ocupa metade da largura em telas médias ou maiores. - O formulário é estilizado dentro de um card com classes
card shadow-sm
, que adicionam bordas arredondadas e sombra. O conteúdo está no corpo do card, identificado porcard-body
. - O título “Pagamento” está no topo do formulário, estilizado com
h3 text-center mb-4
para centralização e espaçamento inferior. - O campo de pagamento está representado por um elemento com
id="payment-element"
. Ele será preenchido dinamicamente com os elementos do Stripe usando JavaScript. - O botão de envio tem as classes
btn btn-primary w-100 mt-4
, que aplicam cor azul, largura total e espaçamento superior. O texto do botão é dinâmico, exibindo algo como “Pagar R$ {{ order.total }}”. - Há um elemento de mensagem de erro identificado por
id="payment-message"
, que usa classes comoalert alert-danger mt-3
para exibição estilizada. Ele começa oculto e é exibido dinamicamente em caso de erro. - Um script do Stripe é carregado diretamente da CDN com
<script src="https://js.stripe.com/v3/"></script>
. - O JavaScript inicializa o Stripe com a chave pública (
publish_key
) e configura os elementos do pagamento. A funçãoinitialize()
cria uma sessão de pagamento no backend e monta os elementos do Stripe dentro de#payment-element
. - A função
handleSubmit()
gerencia a submissão do formulário. Ela confirma o pagamento com a API do Stripe e exibe mensagens de erro ou redireciona para uma página de sucesso, dependendo do resultado. - A função
checkStatus()
verifica o status do pagamento usando o parâmetropayment_intent_client_secret
. Dependendo do status (succeeded
,processing
, ourequires_payment_method
), exibe mensagens apropriadas no elemento#payment-message
. - A função
showMessage()
é responsável por exibir mensagens dinâmicas no elemento#payment-message
. - A função
setLoading(isLoading)
gerencia o estado do botão de envio, desativando-o e alterando o texto enquanto o pagamento está em processamento.
e_commerce/billing/templates/billing/payment-method.html
<{% extends "base.html" %}> <{% load static %}> <{% block content %}>
<{% extends "base.html" %}> <{% load static %}> <{% block content %}>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<form id="payment-form" class="card shadow-sm">
<div class="card-body">
<h3 class="text-center mb-4">Pagamento</h3>
<div id="payment-element"></div>
<button id="submit" class="btn btn-primary w-100 mt-4">
<span id="button-text">Pagar R$ {{ order.total }}</span>
</button>
<div
id="payment-message"
class="alert alert-danger mt-3"
style="display: none"
></div>
</div>
</form>
</div>
</div>
</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe("{{ publish_key }}");
let elements;
initialize();
checkStatus();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
async function initialize() {
const response = await fetch("/billing/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": "{{ csrf_token }}",
},
});
const { clientSecret } = await response.json();
elements = stripe.elements({ clientSecret });
const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.origin + "/billing/payment-success/",
},
});
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("Ocorreu um erro inesperado.");
}
setLoading(false);
}
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Pagamento realizado com sucesso!");
break;
case "processing":
showMessage("Seu pagamento está sendo processado.");
break;
case "requires_payment_method":
showMessage("Seu pagamento não foi bem sucedido, tente novamente.");
break;
default:
showMessage("Algo deu errado.");
break;
}
}
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
messageContainer.textContent = messageText;
messageContainer.style.display = "block";
}
function setLoading(isLoading) {
if (isLoading) {
document.querySelector("#submit").disabled = true;
document.querySelector("#button-text").textContent = "Processando...";
} else {
document.querySelector("#submit").disabled = false;
document.querySelector("#button-text").textContent =
"Pagar R$ {{ order.total }}";
}
}
</script>
<{% endblock %}>
<{% endblock %}>
Explicação do Código – e_commerce/billing/views.py
- Recupera o ID do carrinho da sessão:
cart_id = request.session.get("cart_id")
- Marca o pedido como pago:
order_obj.mark_paid()
- Zera itens no carrinho e remove o ID da sessão:
request.session['cart_items'] = 0
edel request.session['cart_id']
- Bloco try para tratar erros:
try: cart_id = request.session.get("cart_id")
- Verifica se o carrinho existe e redireciona:
if not cart_id: return redirect("cart:home")
- Define contexto com chave Stripe e pedido:
context = {"publish_key": settings.STRIPE_PUB_KEY, "order": order_obj}
- Exclui a verificação CSRF:
@csrf_exempt
- Aceita apenas requisições POST:
@require_POST
- Recupera o ID do carrinho dentro da sessão:
cart_id = request.session.get("cart_id")
- Calcula o valor total do pedido:
total = cart_obj.total + order_obj.shipping_total
- Cria PaymentIntent no Stripe:
intent = stripe.PaymentIntent.create(amount=int(total * 100), currency='brl', automatic_payment_methods={'enabled': True})
- Retorna JSON com clientSecret:
return JsonResponse({'clientSecret': intent.client_secret})
e_commerce/billing/views.py
from django.shortcuts import redirect, render
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from django.views.decorators.csrf import csrf_exempt
from billing.models import BillingProfile
from orders.models import Order
from products.models import Product
from carts.models import Cart
from django.conf import settings
import stripe
from django.contrib import messages
stripe.api_key = settings.STRIPE_API_KEY
def payment_success_view(request):
cart_id = request.session.get("cart_id")
if cart_id:
cart_obj = Cart.objects.get(id=cart_id)
order_obj = Order.objects.filter(cart=cart_obj).first()
if order_obj:
order_obj.mark_paid()
request.session['cart_items'] = 0
del request.session['cart_id']
return render(request, 'billing/payment-success.html')
def payment_failed_view(request):
return render(request, 'billing/payment-failed.html')
def payment_method_view(request):
try:
cart_id = request.session.get("cart_id")
if not cart_id:
print(f"Cart ID não encontrado na sessão: {cart_id}")
return redirect("cart:home")
cart_obj = Cart.objects.filter(id=cart_id).first()
if not cart_obj:
print(f"Cart não encontrado no banco: {cart_id}")
return redirect("cart:home")
order_obj = Order.objects.filter(cart=cart_obj).first()
if not order_obj:
print("Order não encontrado")
return redirect("cart:home")
context = {
"publish_key": settings.STRIPE_PUB_KEY,
"order": order_obj,
}
return render(request, "billing/payment-method.html", context)
except Exception as e:
print(f"Erro: {str(e)}")
return redirect("cart:home")
@csrf_exempt
@require_POST
def create_checkout_session(request):
try:
cart_id = request.session.get("cart_id")
cart_obj = Cart.objects.get(id=cart_id)
order_obj = Order.objects.filter(cart=cart_obj).first()
total = cart_obj.total + order_obj.shipping_total
intent = stripe.PaymentIntent.create(
amount=int(total * 100),
currency='brl',
automatic_payment_methods={'enabled': True},
)
return JsonResponse({
'clientSecret': intent.client_secret
})
except Exception as e:
print(f"Stripe Error: {str(e)}")
return JsonResponse({'error': str(e)}, status=400)
Explicação do Código – search/templates/search/snippets/search-form.html
- Div flexível: Organiza o campo de busca e o botão lado a lado com um espaçamento definido.
- Campo de entrada: Permite ao usuário digitar a busca, com suporte a placeholder e preenchimento automático.
- Botão de envio: Envia os dados do formulário quando clicado, com um design primário destacado.
e_commerce/search/templates/search/snippets/search-form.html
<form method="GET" action="{% url 'search:query' %}" class="form my-2 my-lg-0 ms-auto">
<div class="d-flex gap-2">
<input class="form-control" type="search" placeholder="Search" name="q" aria-label="Search" value="{{ request.GET.q }}">
<button class="btn btn-primary" type="submit">Search</button>
</div>
</form>
Explicação do Código – e_commerce/templates/base/navbar.html
dfgdg
e_commerce/templates/base/navbar.html
{% load static %}
{% url 'home' as home_url %}
{% url 'contact' as contact_url %}
{% url 'products:list' as products_list_url %}
{% url 'login' as login_url %}
{% url 'logout' as logout_url %}
{% url 'register' as register_url %}
{% url 'cart:home' as cart_url %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">
<div class="container">
<a class="navbar-brand" href="{{ home_url }}">
<!-- Uso da logo com a tag 'static' -->
<img src="{% static 'img/logo.png' %}" width="30" height="30" class="d-inline-block align-top" alt="Logo">
{% if nome_da_marca %}
{{ nome_da_marca }}
{% else %} Código Fluente eCommerce
{% endif %}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item {% if request.path == home_url %} active {% endif %}">
<a class="nav-link" href="{{ home_url }}">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item {% if request.path == contact_url %} active {% endif %}">
<a class="nav-link" href="{{ contact_url }}">Contato</a>
</li>
<li class="nav-item {% if request.path == products_list_url %} active {% endif %}">
<a class="nav-link" href="{{ products_list_url }}">Produtos</a>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item {% if request.path == login_url %} active {% endif %}">
<a class="nav-link" href="{{ logout_url }}">Logout</a>
</li>
{% else %}
<li class="nav-item {% if request.path == login_url %} active {% endif %}">
<a class="nav-link" href="{{ login_url }}?next={{request.path}}">Login</a>
</li>
<li class="nav-item {% if request.path == register %} active {% endif %}">
<a class="nav-link" href="{{ register_url }}">Registrar-se</a>
</li>
{% endif %}
<li class="nav-item {% if request.path == cart_url %} active {% endif %}">
<a class="nav-link" href="{{ cart_url }}">
<span class="navbar-cart-count">
{% with request.session.cart_items|default:0 as cart_items %}
{{ cart_items }}
{% endwith %}
</span>
<i class="fa fa-shopping-cart"></i>
</a>
</li>
</ul>
{% include 'search/snippets/search-form.html' %}
</div>
</div><!--fim container-->
</nav>
Explicação do Código – e_commerce/templates/home_page.html
- Título e descrição principal:
O título foi alterado para “Vitrine Digital” e recebeu uma classedisplay-4
para maior destaque e estilo. A descrição agora enfatiza a experiência de compras online, com classelead
para melhorar a tipografia. - Atualização da imagem principal:
O nome do arquivo de imagem foi alterado paraeCommerce.jpg
, e a imagem agora ocupa 100% da largura (w-100
) com altura limitada a 600px, usandoobject-fit: cover
para manter proporções visuais. - Alteração na estrutura e espaçamento:
Foram adicionadas classes de espaçamento comomb-4
emb-5 pb-4
para melhorar a organização visual. Essas alterações ajudam a criar uma página mais atraente e bem estruturada.
Essas mudanças modernizam a página inicial, tornando-a mais visualmente impactante e adaptada a uma experiência de usuário aprimorada.
e_commerce/templates/home_page.html
{% extends "base.html" %}
{% load static %}
{% block base_head %}
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
<title>Vitrine Digital</title>
{% endblock %}
{% block content %}
<div class="text-center mb-4">
<h1 class="display-4 text-primary">{{ title }}</h1>
<p class="lead text-muted">{{ content }}</p>
</div>
<div class="row mb-5 pb-4">
<div class="col">
<img
src="{% static 'img/eCommerce.jpg' %}"
class="img-fluid w-100"
style="max-height: 600px; object-fit: cover"
alt="eCommerce"
/>
</div>
</div>
{% if request.user.is_authenticated %}
<div class="row mt-4">
<div class="col">
<h1>Premium</h1>
<p>{{ premium_content }}</p>
</div>
</div>
{% endif %}
{% endblock %}
Explicação do Código – e_commerce/e_commerce/view.py
- Mudança no título e no conteúdo.
e_commerce/e_commerce/view.py
from django.contrib.auth import authenticate, get_user_model
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect
from .forms import ContactForm
def is_ajax(request):
return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
def home_page(request):
context = {
"title": "Vitrine Digital",
"content": "Sua melhor experiência em compras online",
}
if request.user.is_authenticated:
context["premium_content"] = "Você é um usuário Premium"
return render(request, "home_page.html", context)
def about_page(request):
context = {
"title": "Página Sobre",
"content": "Bem vindo a página sobre"
}
return render(request, "about/view.html", context)
def contact_page(request):
contact_form = ContactForm(request.POST or None)
context = {
"title": "Página de Contato",
"content": "Bem vindo a página de contato",
"form": contact_form
}
if contact_form.is_valid():
print(contact_form.cleaned_data)
if is_ajax(request):
return JsonResponse({"message": "Obrigado!"})
if contact_form.errors:
errors = contact_form.errors.as_json()
if is_ajax(request):
return HttpResponse(errors, status=400, content_type='application/json')
return render(request, "contact/view.html", context)
Explicação do Código – e_commerce/accounts/templates/accounts/login.html
- Carrega o módulo de tags customizadas
custom_tags
para uso no template. - Define uma
div
principal com as classes Bootstrapcontainer
emt-5
para estruturar o conteúdo. - Exibe mensagens de feedback (
messages
) em um componente toast do Bootstrap, com classes e estilos dinâmicos baseados nostags
das mensagens (success
,error
,warning
, etc.). - Estrutura o formulário de login dentro de uma
div
centralizada com classes comorow justify-content-center
ecard shadow-sm
para estilização e layout. - Exibe o título “Login” com classes Bootstrap para estilização, como
text-center
,mb-4
, etext-primary
. - Define a ação do formulário com a URL gerada dinamicamente para
accounts:login
usando a tag{% url %}
. - Usa o filtro customizado
add_class
para adicionar classes Bootstrap comoform-control form-control-lg
aos camposemail
epassword
do formulário. - Adiciona uma opção “Lembrar senha” com um checkbox estilizado usando classes Bootstrap
form-check-input
eform-check-label
. - Inclui um campo oculto para o parâmetro
next
, que preserva o redirecionamento após o login, baseado no valor derequest.GET.next
. - Estiliza o botão de envio com classes Bootstrap
btn btn-primary btn-lg w-100
para criar um botão grande, azul, e com largura total.
e_commerce/accounts/templates/accounts/login.html
<{% extends "base.html" %}>
<{% load custom_tags %}>
<{% block content %}>
<div class="container mt-5">
<!-- Toast para mensagens -->
<{% if messages %}>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<{% for message in messages %}>
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="3000">
<{% if 'success' in message.tags %}>
<div class="toast-header bg-success text-white">
<{% elif 'error' in message.tags %}>
<div class="toast-header bg-danger text-white">
<{% elif 'warning' in message.tags %}>
<div class="toast-header bg-warning text-dark">
<{% else %}>
<div class="toast-header bg-info text-white">
<{% endif %}>
<strong class="me-auto">Mensagem</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ message }}
</div>
</div>
<{% endfor %}>
</div>
<{% endif %}>
<div class="row justify-content-center">
<div class="col-md-6 col-12">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="text-center mb-4 text-primary">Login</h2>
<form method="POST" action="{% url 'accounts:login' %}">
<{% csrf_token %}>
<div class="form-group mb-3">
<label for="id_email" class="form-label fw-bold">Email</label>
{{ form.email|add_class:"form-control form-control-lg" }}
<small id="emailHelp" class="form-text text-muted">
Nunca compartilharemos seu e-mail com mais ninguém.
</small>
</div>
<div class="form-group mb-3">
<label for="id_password" class="form-label fw-bold">Senha</label>
{{ form.password|add_class:"form-control form-control-lg" }}
</div>
<div class="form-group form-check mb-3">
<input type="checkbox" class="form-check-input" id="remember_me" name="remember_me" />
<label class="form-check-label" for="remember_me">Lembrar senha</label>
</div>
<input type="hidden" name="next" value="{{ request.GET.next }}" />
<button type="submit" class="btn btn-primary btn-lg w-100">Enviar</button>
</form>
</div>
</div>
</div>
</div>
</div>
<{% endblock %}>
Explicação do Código – e_commerce/accounts/templates/accounts/register.html
{% load custom_tags %}
: Carrega tags customizadas definidas em um módulo de tags personalizado para uso no template.<div class="container mt-5">
: Cria um contêiner principal com margem superior (mt-5
) para organizar o layout da página.- Toast para mensagens: Exibe mensagens de feedback (como sucesso, erro, ou aviso) em uma interface toast estilizada com Bootstrap. O estilo e o conteúdo variam com base nos
tags
da mensagem. - Formulário de registro: Cria o layout do formulário de registro, centralizado com classes Bootstrap como
row justify-content-center
. <h2 class="text-center mb-4 text-primary">Registrar</h2>
: Adiciona o título “Registrar” no centro, com margem inferior (mb-4
) e cor primária (text-primary
).action="{% url 'accounts:register' %}"
: Define o destino do formulário como a URL nomeada'accounts:register'
, gerada dinamicamente.<label for="{{ form.email.id_for_label }}" class="form-label fw-bold">Email</label>
: Define o rótulo do campo de email, associado ao ID do campo gerado dinamicamente pelo formulário.{{ form.email|add_class:"form-control form-control-lg" }}
: Renderiza o campo de email e adiciona as classes Bootstrapform-control
eform-control-lg
para estilização.<label for="{{ form.full_name.id_for_label }}" class="form-label fw-bold">Nome Completo</label>
: Define o rótulo para o campo de nome completo, com associação ao ID gerado dinamicamente.{{ form.full_name|add_class:"form-control form-control-lg" }}
: Renderiza o campo de nome completo com as mesmas classes de estilização.<label for="{{ form.password.id_for_label }}" class="form-label fw-bold">Senha</label>
: Define o rótulo para o campo de senha.{{ form.password|add_class:"form-control form-control-lg" }}
: Renderiza o campo de senha estilizado com Bootstrap.<label for="{{ form.password_2.id_for_label }}" class="form-label fw-bold">Confirmar Senha</label>
: Define o rótulo para o campo de confirmação de senha.{{ form.password_2|add_class:"form-control form-control-lg" }}
: Renderiza o campo de confirmação de senha com as mesmas classes de estilização.
e_commerce/accounts/templates/accounts/register.html
<{% extends "base.html" %}>
<{% load custom_tags %}>
<{% block content %}>
<div class="container mt-5">
<!-- Toast para mensagens -->
<{% if messages %}>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<{% for message in messages %}>
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="true" data-bs-delay="3000">
<{% if 'success' in message.tags %}>
<div class="toast-header bg-success text-white">
<{% elif 'error' in message.tags %}>
<div class="toast-header bg-danger text-white">
<{% elif 'warning' in message.tags %}>
<div class="toast-header bg-warning text-dark">
<{% else %}>
<div class="toast-header bg-info text-white">
<{% endif %}>
<strong class="me-auto">Mensagem</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{{ message }}
</div>
</div>
<{% endfor %}>
</div>
<{% endif %}>
<!-- Formulário de registro -->
<div class="row justify-content-center">
<div class="col-md-6 col-12">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="text-center mb-4 text-primary">Registrar</h2>
<form method="POST" action="{% url 'accounts:register' %}">
<{% csrf_token %}>
<div class="form-group mb-3">
<label for="{{ form.email.id_for_label }}" class="form-label fw-bold">Email</label>
{{ form.email|add_class:"form-control form-control-lg" }}
<small id="emailHelp" class="form-text text-muted">
Nunca compartilharemos seu e-mail com mais ninguém.
</small>
</div>
<div class="form-group mb-3">
<label for="{{ form.full_name.id_for_label }}" class="form-label fw-bold">Nome Completo</label>
{{ form.full_name|add_class:"form-control form-control-lg" }}
</div>
<div class="form-group mb-3">
<label for="{{ form.password.id_for_label }}" class="form-label fw-bold">Senha</label>
{{ form.password|add_class:"form-control form-control-lg" }}
</div>
<div class="form-group mb-3">
<label for="{{ form.password_2.id_for_label }}" class="form-label fw-bold">Confirmar Senha</label>
{{ form.password_2|add_class:"form-control form-control-lg" }}
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">Enviar</button>
</form>
</div>
</div>
</div>
</div>
</div>
<{% endblock %}>
Explicação do Código – e_commerce/accounts/templates/accounts/snippets/form.html
{% load custom_tags %}
: Carrega tags customizadas definidas em um módulo específico para uso no template.accounts:login
: Nome da rota utilizada para definir a URL padrão de ação do formulário casoaction_url
não esteja presente.- Itera pelos campos do formulário: Itera dinamicamente por todos os campos do formulário, renderizando-os com estilização e mensagens de erro.
btn-primary btn-lg w-100
: Classes Bootstrap aplicadas ao botão para defini-lo como azul (primário), grande (btn-lg
), e com largura total (w-100
).Enviar
: Texto exibido no botão de envio do formulário.
e_commerce/accounts/templates/accounts/snippets/form.html
<{% load static %}>
<{% load custom_tags %}>
<form method="POST"
action="{% if action_url %}{{ action_url }}{% else %}{% url 'accounts:login' %}{% endif %}">
<{% csrf_token %}>
<{% if next_url %}>
<input type="hidden" name="next" value="{{ next_url }}" />
<{% endif %}>
<!-- Itera pelos campos do formulário -->
<{% for field in form %}>
<div class="form-group mb-3">
<label for="{{ field.id_for_label }}" class="form-label fw-bold">{{ field.label }}</label>
{{ field|add_class:"form-control form-control-lg" }}
<{% if field.help_text %}>
<small class="form-text text-muted">{{ field.help_text }}</small>
<{% endif %}>
<{% if field.errors %}>
<div class="text-danger small">{{ field.errors }}</div>
<{% endif %}>
</div>
<{% endfor %}>
<button type="submit" class="btn btn-primary btn-lg w-100">Enviar</button>
</form>
Crie o accounts/templatetags/__init__.py vazio.
E o accounts/templatetags/custom_tags.py.
Explicação do Custom Filters do Carts – e_commerce/accounts/templatetags/custom_tags.py
Este código cria um filtro personalizado para adicionar classes CSS a campos de formulário no Django:
- Importações:
template.Library
: Permite registrar filtros personalizados.BoundField
: Representa campos de formulário vinculados.
- Registro:
register = template.Library()
: Cria o registro para os filtros.
- Filtro
add_class
:- Adiciona uma classe CSS a um campo de formulário.
- Verifica se o campo é uma instância de
BoundField
. - Se for, renderiza o campo como widget com o atributo
class
e a classe especificada. - Se não for, retorna o campo original sem alterações.
- Uso em templates:
- Exemplo:
{{ form.email|add_class:"form-control" }}
.
- Exemplo:
- Benefícios:
- Simplifica a adição de estilos CSS diretamente nos templates.
- Melhora a separação entre lógica de formulário e apresentação visual.
e_commerce/accounts/templatetags/custom_tags.py
from django import template
register = template.Library()
@register.filter
def add_class(field, css_class):
"""
Adiciona uma classe CSS a um campo de formulário.
"""
return field.as_widget(attrs={"class": css_class})
Explicação do Código – e_commerce/carts/models.py
- Modelo intermediário
CartProduct
:
Um novo modelo foi adicionado para gerenciar a relação entreCart
eProduct
. Isso permite adicionar o campoquantity
para rastrear quantidades individuais de produtos no carrinho. - Alteração no relacionamento
products
no modeloCart
:
O campoproducts
agora utiliza o modelo intermediárioCartProduct
, permitindo associar informações extras, como quantidade, a cada produto no carrinho. - Método
update_totals
no modeloCart
:
A lógica de atualização de totais foi consolidada em um único método, calculando o subtotal com base nas quantidades e nos preços dos produtos. Isso facilita a manutenção e reutilização do código. - Remoção do sinal
m2m_changed_cart_receiver
:
A lógica anteriormente gerenciada pelo sinal foi substituída pelo métodoupdate_totals
, que é chamado diretamente quando necessário. Isso simplifica o código e reduz dependências de sinais.
Essas mudanças tornam o gerenciamento do carrinho mais robusto e escalável, permitindo lidar com quantidades de produtos de forma eficiente e centralizada.
e_commerce/carts/models.py
from decimal import Decimal
from django.conf import settings
from django.db import models
from django.db.models.signals import pre_save
from products.models import Product
User = settings.AUTH_USER_MODEL
class CartProduct(models.Model):
"""
Modelo intermediário para gerenciar produtos no carrinho com quantidades.
"""
cart = models.ForeignKey('Cart', on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return f"{self.quantity} x {self.product.title}"
class CartManager(models.Manager):
def new_or_get(self, request):
cart_id = request.session.get("cart_id", None)
qs = self.get_queryset().filter(id=cart_id)
if qs.count() == 1:
new_obj = False
cart_obj = qs.first()
if request.user.is_authenticated and cart_obj.user is None:
cart_obj.user = request.user
cart_obj.save()
else:
cart_obj = self.new(user=request.user)
new_obj = True
request.session['cart_id'] = cart_obj.id
return cart_obj, new_obj
def new(self, user=None):
user_obj = None
if user is not None and user.is_authenticated:
user_obj = user
return self.model.objects.create(user=user_obj)
class Cart(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
products = models.ManyToManyField(Product, through='CartProduct', blank=True)
subtotal = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
total = models.DecimalField(default=0.00, max_digits=100, decimal_places=2)
updated = models.DateTimeField(auto_now=True)
timestamp = models.DateTimeField(auto_now_add=True)
objects = CartManager()
def __str__(self):
return str(self.id)
def update_totals(self):
"""
Atualiza os totais do carrinho com base na quantidade dos produtos.
"""
subtotal = sum(
cart_product.product.get_final_price() * cart_product.quantity
for cart_product in self.cartproduct_set.all()
)
self.subtotal = subtotal
self.total = subtotal # Substitua se tiver taxa de entrega
self.save()
def pre_save_cart_receiver(sender, instance, *args, **kwargs):
if instance.subtotal > 0:
instance.total = Decimal(instance.subtotal) # Adicione taxa se necessário
else:
instance.total = 0.00
pre_save.connect(pre_save_cart_receiver, sender=Cart)
Explicação do Código – e_commerce/carts/views.py
- Adicionada a importação de
get_object_or_404
para simplificar a validação e obtenção de objetos existentes no banco de dados. - Adicionado o modelo
CartProduct
para gerenciar produtos associados ao carrinho e permitir o controle de quantidade. - Introduzida a função
add_to_cart
, que:- Lida com a lógica de adicionar ou atualizar produtos no carrinho.
- Valida a quantidade enviada na requisição.
- Cria ou atualiza uma instância de
CartProduct
com base no carrinho e produto. - Atualiza os totais do carrinho e o número total de itens na sessão do usuário.
- Retorna um JSON com o status da operação e o total de itens no carrinho.
- Atualizada a função
cart_update
para:- Suportar a alteração de quantidade dos produtos no carrinho.
- Remover produtos caso a quantidade seja zero.
- Utilizar o modelo
CartProduct
para controlar a quantidade de itens. - Retornar um JSON com o status da operação, número de itens, subtotal e total do carrinho formatados.
Essas mudanças adicionam funcionalidades mais avançadas e robustas para a gestão de carrinho de compras, permitindo controle granular sobre os produtos e suas quantidades.
e_commerce/carts/views.py
from django.http import JsonResponse, Http404
from django.shortcuts import render, redirect, get_object_or_404
from accounts.forms import LoginForm, GuestForm
from accounts.models import GuestEmail
from addresses.forms import AddressForm
from addresses.models import Address
from billing.models import BillingProfile
from orders.models import Order
from products.models import Product
from .models import Cart, CartProduct
def is_ajax(request):
return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'
def add_to_cart(request):
if request.method == "POST":
# Pegando os dados do POST
product_id = request.POST.get("product_id")
quantity = request.POST.get("quantity", 1)
# Validando os dados
try:
quantity = int(quantity)
except ValueError:
return JsonResponse({"success": False, "message": "Quantidade inválida."})
# Verificando se o produto existe
product = get_object_or_404(Product, id=product_id)
# Obtendo ou criando o carrinho
cart_obj, _ = Cart.objects.new_or_get(request)
# Adicionando ou atualizando o produto no carrinho
cart_product, created = CartProduct.objects.get_or_create(cart=cart_obj, product=product)
if not created:
cart_product.quantity += quantity
else:
cart_product.quantity = quantity
cart_product.save()
# Atualizando o total do carrinho
cart_obj.update_totals()
# Calculando o total de itens no carrinho
total_items = sum(item.quantity for item in cart_obj.cartproduct_set.all())
request.session['cart_items'] = total_items # Atualizando a sessão
return JsonResponse({"success": True, "total_items": total_items})
# Resposta para requisições que não são POST
return JsonResponse({"success": False, "message": "Método não permitido."})
def cart_home(request):
cart_obj, new_obj = Cart.objects.new_or_get(request)
return render(request, "carts/home.html", {"cart": cart_obj})
from django.http import JsonResponse
def cart_get_items(request):
cart_obj, new_obj = Cart.objects.new_or_get(request)
items = []
for product in cart_obj.products.all():
item = {
'id': product.id,
'name': product.title,
'quantity': 1,
'price': str(product.price),
}
items.append(item)
return JsonResponse({'items': items})
def cart_update(request):
cart_obj, new_obj = Cart.objects.new_or_get(request)
product_id = request.POST.get("product_id")
quantity = request.POST.get("quantity", 1)
try:
quantity = int(quantity)
except ValueError:
quantity = 1
product_obj = get_object_or_404(Product, id=product_id)
cart_product, created = CartProduct.objects.get_or_create(cart=cart_obj, product=product_obj)
if quantity > 0:
cart_product.quantity = quantity
cart_product.save()
else:
cart_product.delete()
cart_obj.update_totals()
request.session['cart_items'] = sum(item.quantity for item in cart_obj.cartproduct_set.all())
return JsonResponse({
"success": True,
"cartItemCount": request.session['cart_items'],
"subtotal": f"{cart_obj.subtotal:.2f}", # Subtotal formatado como string
"total": f"{cart_obj.total:.2f}", # Total formatado como string
})
def checkout_home(request):
#aqui a gente pega o carrinho
cart_obj, cart_created = Cart.objects.new_or_get(request)
order_obj = None
#se o carrinho acabou de ser criado, ele tá zerado
#ou se o carrinho já existir mas não tiver nada dentro
if cart_created or cart_obj.products.count() == 0:
return redirect("cart:home")
login_form = LoginForm()
guest_form = GuestForm()
address_form = AddressForm()
billing_address_id = request.session.get("billing_address_id", None)
shipping_address_id = request.session.get("shipping_address_id", None)
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
address_qs = None
if billing_profile is not None:
if request.user.is_authenticated:
address_qs = Address.objects.filter(billing_profile=billing_profile)
order_obj, order_obj_created = Order.objects.new_or_get(billing_profile, cart_obj)
if shipping_address_id:
order_obj.shipping_address = Address.objects.get(id = shipping_address_id)
del request.session["shipping_address_id"]
if billing_address_id:
order_obj.billing_address = Address.objects.get(id = billing_address_id)
del request.session["billing_address_id"]
if billing_address_id or shipping_address_id:
order_obj.save()
if request.method == "POST":
#verifica se o pedido foi feito
is_done = order_obj.check_done()
if is_done:
order_obj.mark_paid()
request.session['cart_items'] = 0
del request.session['cart_id']
return redirect("cart:success")
context = {
"object": order_obj,
"billing_profile": billing_profile,
"login_form": login_form,
"guest_form": guest_form,
"address_form": address_form,
"address_qs": address_qs,
}
return render(request, "carts/checkout.html", context)
def checkout_done_view(request):
return render(request, "carts/checkout-done.html", {})
Crie o carts/templatetags/__init__.py vazio.
E o carts/templatetags/custom_filters.py.
Explicação do Custom Filters do Carts – e_commerce/carts/templatetags/custom_filters.py
- Registro de biblioteca de templates: A variável
register
é uma instância detemplate.Library
. Ela registra os filtros customizados que poderão ser usados em templates Django. - Filtro
multiply
:- Função: Multiplica dois valores (o valor inicial
value
e o argumentoarg
) fornecidos no template. - Tratamento de erros: Se os valores não puderem ser convertidos para
float
ou forem inválidos, retorna0
. - Uso em templates:
{{ value|multiply:arg }}
para multiplicarvalue
porarg
.
- Função: Multiplica dois valores (o valor inicial
- Filtro
add_class
:- Função: Adiciona uma classe CSS a um campo de formulário Django.
- Como funciona: Altera o método
as_widget
do campo para incluir o atributoclass
com o valor fornecido comocss_class
. - Tratamento de erros: Se o campo não suportar o método
as_widget
(como no caso de um texto comum em vez de um campo de formulário), ele retorna o campo sem alterações. - Uso em templates:
{{ form.field_name|add_class:"form-control" }}
para aplicar a classeform-control
ao campo de formulário.
Esses filtros ajudam a melhorar a reutilização e estilização de formulários e cálculos diretamente nos templates.
- Registro de biblioteca de templates: A variável
e_commerce/carts/templatetags/custom_filters.py
from django import template
register = template.Library()
@register.filter
def multiply(value, arg):
"""
Multiplica o valor (value) pelo argumento (arg).
"""
try:
return float(value) * float(arg)
except (ValueError, TypeError):
return 0
@register.filter
def add_class(field, css_class):
"""
Adiciona uma classe CSS a um campo de formulário.
"""
return field.as_widget(attrs={"class": css_class})
Explicação do Código – e_commerce/carts/urls.py
A mudança adicionou uma nova rota ao arquivo urls.py
, associada à funcionalidade de adicionar produtos ao carrinho com quantidades específicas:
- Adição da rota
add_to_cart
:- A rota
"add/"
foi configurada para chamar a nova viewadd_to_cart
com o nomeadd_to_cart
. - Essa rota permite que o frontend envie requisições para adicionar ou atualizar produtos no carrinho, utilizando quantidades específicas fornecidas pelo usuário.
- A rota
Essa modificação é importante porque melhora a experiência do usuário ao possibilitar o gerenciamento detalhado dos produtos no carrinho, com suporte para operações como adicionar múltiplos itens de uma vez ou ajustar a quantidade diretamente. Isso também facilita a integração com o novo modelo intermediário CartProduct
, que agora gerencia as quantidades e associações de produtos ao carrinho.
e_commerce/carts/urls.py
from django.urls import path
from .views import (
cart_home,
checkout_home,
cart_update,
checkout_done_view,
cart_get_items,
add_to_cart,
)
app_name = "carts"
urlpatterns = [
path('', cart_home, name='home'),
path('checkout/success/', checkout_done_view, name='success'),
path('checkout/', checkout_home, name='checkout'),
path('update/', cart_update, name='update'),
path('get-items/', cart_get_items, name='cart-get-items'),
path('add/', add_to_cart, name='add_to_cart'),
]
Explicação do Código – e_commerce/addresses/models.py
- Substituição de
address_line_1
porstreet
. - Remoção de
address_line_2
e inclusão de novos campos:complement
,neighborhood
, enumber
. - Adição de
default=""
em campos obrigatórios comostreet
,city
,state
, epostal_code
. - Ajuste no método
__str__
para exibir o endereço completo com os novos campos. - Modificação no método
get_address
para usar uma lista dinâmica (address_parts
), removendo valores vazios automaticamente. - Maior flexibilidade e detalhamento no modelo de endereço com novos campos e melhor formatação.
e_commerce/addresses/models.py
from django.db import models
from billing.models import BillingProfile
ADDRESS_TYPES = (
('billing', 'Billing'),
('shipping', 'Shipping'),
)
class Address(models.Model):
billing_profile = models.ForeignKey(
BillingProfile,
on_delete=models.CASCADE,
null=True, # Permite nulo temporariamente
blank=True # Permite formulários vazios temporariamente
)
address_type = models.CharField(
max_length=120,
choices=ADDRESS_TYPES,
default="shipping"
)
street = models.CharField(max_length=255, null=False, blank=False, default="")
complement = models.CharField(max_length=255, null=True, blank=True, default="")
neighborhood = models.CharField(max_length=255, null=True, blank=True, default="")
number = models.CharField(max_length=10, null=True, blank=True, default="")
city = models.CharField(max_length=100, null=False, blank=False, default="")
state = models.CharField(max_length=100, null=False, blank=False, default="")
country = models.CharField(max_length=100, null=False, blank=False, default="")
postal_code = models.CharField(max_length=20, null=False, blank=False, default="")
def __str__(self):
return f"{self.street}, {self.number} - {self.city}, {self.state} ({self.postal_code})"
def get_address(self):
address_parts = [
f"{self.street}, {self.number}",
self.complement if self.complement else "",
self.neighborhood if self.neighborhood else "",
f"{self.city}, {self.state}",
self.postal_code,
self.country
]
return "\n".join(filter(None, address_parts))
Explicação do Código – e_commerce/addresses/views.py
e_commerce/addresses/views.py
from django.shortcuts import render, redirect
from django.utils.http import url_has_allowed_host_and_scheme
from billing.models import BillingProfile
from .forms import AddressForm
from .models import Address
from orders.models import Order
def checkout_address_create_view(request):
print("========= DEBUG INFO =========")
print("Method:", request.method)
print("POST data:", request.POST)
print("Address Type:", request.POST.get('address_type'))
form = AddressForm(request.POST or None)
print("Form is valid:", form.is_valid())
print("============================")
if form.is_valid():
instance = form.save(commit=False)
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
if billing_profile is not None:
address_type = request.POST.get('address_type', 'shipping')
print("Address type after validation:", address_type)
instance.billing_profile = billing_profile
instance.address_type = address_type
instance.save()
request.session[address_type + "_address_id"] = instance.id
# Se for endereço de cobrança, vai para pagamento
if address_type == "billing":
print("Redirecting to payment...")
return redirect('billing-payment-method')
return redirect('cart:checkout')
def checkout_address_reuse_view(request):
if request.user.is_authenticated and request.method == "POST":
billing_profile, billing_profile_created = BillingProfile.objects.new_or_get(request)
shipping_address = request.POST.get('shipping_address')
address_type = request.POST.get('address_type')
if shipping_address:
address = Address.objects.filter(billing_profile=billing_profile, id=shipping_address).first()
if address:
order_obj = Order.objects.filter(billing_profile=billing_profile, cart_id=request.session.get('cart_id')).first()
if order_obj:
if address_type == 'shipping':
order_obj.shipping_address = address
else:
order_obj.billing_address = address
order_obj.save()
next_ = request.POST.get('next')
if url_has_allowed_host_and_scheme(next_, request.get_host()):
return redirect(next_)
return redirect("cart:checkout")
Explicação do Código – e_commerce/addresses/forms.py
'street', 'number', 'complement', 'neighborhood'
: Campos do modeloAddress
incluídos no formulário para captura de informações como rua, número, complemento e bairro.labels
: Define rótulos personalizados para os campos exibidos no formulário, como “Rua” parastreet
, “Número” paranumber
, e assim por diante.widgets
: Aplica widgets personalizados aos campos para estilização, usando a classe CSSform-control
para cada campo, garantindo consistência visual no formulário.def clean_street(self):
: Método de validação para o campostreet
, que verifica se o valor foi fornecido. Caso contrário, gera um erro de validação com a mensagem “Rua é obrigatória.”def clean_city(self):
: Método de validação para o campocity
, verificando se foi preenchido. Se não, gera um erro de validação com a mensagem “Cidade é obrigatória.”def clean_postal_code(self):
: Método de validação para o campopostal_code
, verificando se o CEP foi fornecido. Se não, retorna o erro “CEP é obrigatório.”
e_commerce/addresses/forms.py
from django import forms
from .models import Address
class AddressForm(forms.ModelForm):
class Meta:
model = Address
fields = [
'street',
'number',
'complement',
'neighborhood',
'city',
'state',
'country',
'postal_code'
]
labels = {
'street': 'Rua',
'number': 'Número',
'complement': 'Complemento',
'neighborhood': 'Bairro',
'city': 'Cidade',
'state': 'Estado',
'country': 'País',
'postal_code': 'CEP'
}
widgets = {
'street': forms.TextInput(attrs={'class': 'form-control'}),
'number': forms.TextInput(attrs={'class': 'form-control'}),
'complement': forms.TextInput(attrs={'class': 'form-control'}),
'neighborhood': forms.TextInput(attrs={'class': 'form-control'}),
'city': forms.TextInput(attrs={'class': 'form-control'}),
'state': forms.TextInput(attrs={'class': 'form-control'}),
'country': forms.TextInput(attrs={'class': 'form-control'}),
'postal_code': forms.TextInput(attrs={'class': 'form-control'})
}
def clean_street(self):
street = self.cleaned_data.get('street')
if not street:
raise forms.ValidationError("Rua é obrigatória.")
return street
def clean_city(self):
city = self.cleaned_data.get('city')
if not city:
raise forms.ValidationError("Cidade é obrigatória.")
return city
def clean_postal_code(self):
postal_code = self.cleaned_data.get('postal_code')
if not postal_code:
raise forms.ValidationError("CEP é obrigatório.")
return postal_code
Explicação do Código – e_commerce/addresses/templates/addresses/form.html
<form method='POST' action='{% if action_url %}{{ action_url }}{% else %}{% url "login" %}{% endif %}'>
: Define o método do formulário comoPOST
e configura a URL de ação. Seaction_url
estiver definido, será usado; caso contrário, a URL será gerada dinamicamente com o nome de rota"login"
.{% csrf_token %}
: Adiciona um token CSRF para proteger o formulário contra ataques CSRF.{% if next_url %}
: Verifica se há um valor paranext_url
. Se existir, adiciona um campo oculto no formulário para preservar o valor e redirecionar o usuário após a submissão.<input type='hidden' name='next' value='{{ next_url }}' />
: Campo oculto que armazena o valor denext_url
, usado para redirecionar o usuário após o login.{% if address_type %}
: Verifica seaddress_type
está definido. Se estiver, adiciona um campo oculto no formulário para preservar o tipo de endereço.<input type='hidden' name='address_type' value='{{ address_type }}' />
: Campo oculto que armazena o tipo de endereço, como “billing” ou “shipping.”{{ form.as_p }}
: Renderiza os campos do formulário, encapsulando-os em tags<p>
.<button type='submit' class='btn btn-default'>Enviar</button>
: Adiciona um botão para enviar o formulário com as classes CSSbtn btn-default
para estilização. O texto do botão é “Enviar.”
e_commerce/addresses/templates/addresses/form.html
<form method='POST' action='{% if action_url %}{{ action_url }}{% else %}{% url "login" %}{% endif %}'>
<{% csrf_token %}>
<{% if next_url %}>
<input type='hidden' name='next' value='{{ next_url }}' />
<{% endif %}>
<{% if address_type %}>
<input type='hidden' name='address_type' value='{{ address_type }}' />
<{% endif %}>
{{ form.as_p }}
<button type='submit' class='btn btn-default'>Enviar</button>
</form>
Explicação do Código – e_commerce/addresses/templates/addresses/prev_addresses.html
{% if address_qs.exists %}
: Verifica se a query setaddress_qs
contém endereços cadastrados. Se existirem, renderiza o formulário; caso contrário, nada será exibido.<form method='POST' action='{{ action_url }}'>
: Inicia o formulário com o métodoPOST
e define a URL de ação como o valor deaction_url
.{% if next_url %}
: Verifica se existe um valor paranext_url
. Se sim, insere um campo oculto no formulário para redirecionamento após a submissão.<input type='hidden' name='next' value='{{ next_url }}' />
: Campo oculto que armazena o valor denext_url
, usado para redirecionar o usuário após o envio do formulário.{% if address_type %}
: Verifica se há um valor paraaddress_type
. Se presente, adiciona um campo oculto para armazenar o tipo de endereço (por exemplo, “shipping” ou “billing”).<input type='hidden' name='address_type' value='{{ address_type }}' />
: Campo oculto que define o tipo de endereço a ser enviado junto ao formulário.{% for address in address_qs %}
: Itera sobre os endereços disponíveis no query setaddress_qs
, gerando opções para o usuário selecionar.<label for='address-{{ address.id }}' class="d-block mb-2">
: Cria um rótulo para cada endereço com uma classe Bootstrap (d-block mb-2
) para espaçamento e layout.<input id='address-{{ address.id }}' type='radio' name='shipping_address' value='{{ address.id }}' class="me-2" />
: Define uma opção de rádio associada ao endereço. O valor será oid
do endereço, permitindo ao usuário escolher qual endereço usar.{{ address.street }}, {{ address.number }} - {{ address.city }}, {{ address.state }}
: Exibe informações do endereço, incluindo rua, número, cidade e estado, para identificação do endereço pelo usuário.<button type='submit' class='btn btn-primary w-100'>Usar Endereço</button>
: Botão de envio do formulário, estilizado com classes Bootstrapbtn btn-primary w-100
para torná-lo azul e com largura total. O texto “Usar Endereço” indica a ação.
e_commerce/addresses/templates/addresses/prev_addresses.html
<{% if address_qs.exists %}>
<form method='POST' action='{{ action_url }}'>
<{% csrf_token %}>
<{% if next_url %}>
<input type='hidden' name='next' value='{{ next_url }}' />
<{% endif %}>
<{% if address_type %}>
<input type='hidden' name='address_type' value='{{ address_type }}' />
<{% endif %}>
<{% for address in address_qs %}>
<label for='address-{{ address.id }}' class="d-block mb-2">
<input id='address-{{ address.id }}' type='radio' name='shipping_address' value='{{ address.id }}' class="me-2" />
{{ address.street }}, {{ address.number }} - {{ address.city }}, {{ address.state }}
</label>
<{% endfor %}>
<button type='submit' class='btn btn-primary w-100'>Usar Endereço</button>
</form>
<{% endif %}>
Deletando o banco de dados SQLite e iniciando um novo
Como fizemos muitas mudanças na estrutura do banco de dados, é recomendável deletar o banco SQLite existente e começar do zero. Isso garante que todas as alterações feitas nos modelos sejam aplicadas corretamente.
Passos para deletar o banco de dados:
Localize o arquivo do banco SQLite: O banco de dados SQLite está localizado na raiz do seu projeto, com o nome db.sqlite3
.
Delete o arquivo:
- No seu sistema operacional, navegue até a pasta do projeto e exclua o arquivo
db.sqlite3
. - Dica: Tenha cuidado ao excluir arquivos. Certifique-se de que está deletando o arquivo correto.
Limpe as migrações antigas:
- Para garantir que as migrações antigas não causem problemas, você pode limpar os arquivos de migração.
- Vá até as pastas de migração de cada app, como
django_ecommerce/<nome_do_app>/migrations/
, e delete todos os arquivos, exceto o__init__.py
.
Criando um novo banco de dados e aplicando as migrações:
Rode os comandos para criar um novo banco com base nos modelos atualizados.
No terminal, rode o comando para criar a migração:
python manage.py makemigrations
Em seguida, aplique a migração para atualizar o banco de dados:
python manage.py migrate
Inserindo dados no novo banco:
e_commerce/script.sql
-- Insert Categories
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Calçados', 'calcados', 'categories/calcados.jpg', 1, NOW());
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Roupas', 'roupas', 'categories/roupas.jpg', 1, NOW());
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Acessórios', 'acessorios', 'categories/acessorios.jpg', 1, NOW());
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Eletrônicos', 'eletronicos', 'categories/eletronicos.jpg', 1, NOW());
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Móveis', 'moveis', 'categories/moveis.jpg', 1, NOW());
INSERT INTO categories_category (name, slug, image, active, timestamp)
VALUES ('Decoração', 'decoracao', 'categories/decoracao.jpg', 1, NOW());
-- Insert Products
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Tênis Esportivo', 'tenis-esportivo', 299.90, 50, (SELECT id FROM categories_category WHERE slug = 'calcados'), 'products/tenis-esportivo.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Camiseta Casual', 'camiseta-casual', 89.90, 100, (SELECT id FROM categories_category WHERE slug = 'roupas'), 'products/camiseta-casual.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Jaqueta de Couro', 'jaqueta-de-couro', 599.90, 30, (SELECT id FROM categories_category WHERE slug = 'roupas'), 'products/jaqueta-de-couro.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Relógio Clássico', 'relogio-classico', 199.90, 40, (SELECT id FROM categories_category WHERE slug = 'acessorios'), 'products/relogio-classico.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Bolsa de Couro', 'bolsa-de-couro', 399.90, 25, (SELECT id FROM categories_category WHERE slug = 'acessorios'), 'products/bolsa-de-couro.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Óculos de Sol', 'oculos-de-sol', 159.90, 60, (SELECT id FROM categories_category WHERE slug = 'acessorios'), 'products/oculos-de-sol.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Notebook Gamer', 'notebook-gamer', 5999.90, 10, (SELECT id FROM categories_category WHERE slug = 'eletronicos'), 'products/notebook-gamer.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Smartphone Android', 'smartphone-android', 1999.90, 15, (SELECT id FROM categories_category WHERE slug = 'eletronicos'), 'products/smartphone-android.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Fone de Ouvido Bluetooth', 'fone-de-ouvido-bluetooth', 299.90, 75, (SELECT id FROM categories_category WHERE slug = 'eletronicos'), 'products/fone-de-ouvido-bluetooth.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Smartwatch Fitness', 'smartwatch-fitness', 499.90, 35, (SELECT id FROM categories_category WHERE slug = 'eletronicos'), 'products/smartwatch-fitness.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Mesa de Escritório', 'mesa-de-escritorio', 799.90, 20, (SELECT id FROM categories_category WHERE slug = 'moveis'), 'products/mesa-de-escritorio.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Cadeira Gamer', 'cadeira-gamer', 999.90, 15, (SELECT id FROM categories_category WHERE slug = 'moveis'), 'products/cadeira-gamer.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Abajur Moderno', 'abajur-moderno', 149.90, 40, (SELECT id FROM categories_category WHERE slug = 'decoracao'), 'products/abajur-moderno.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Quadro Abstrato', 'quadro-abstrato', 259.90, 25, (SELECT id FROM categories_category WHERE slug = 'decoracao'), 'products/quadro-abstrato.jpg', 1, 0, NOW());
INSERT INTO products_product (title, slug, price, stock, category_id, image, active, featured, timestamp)
VALUES ('Tênis de Corrida', 'tenis-de-corrida', 349.90, 45, (SELECT id FROM categories_category WHERE slug = 'calcados'), 'products/tenis-de-corrida.jpg', 1, 0, NOW());
Obs. As imagens desses produtos estão na pasta django_ecommerce/static_cdn/media_root/products e as das categorias django_ecommerce/static_cdn/media_root/categories.
Agora que o banco está pronto, você pode inserir os dados iniciais que configuramos nesse arquivo SQL acima.
Certifique-se de que o arquivo SQL gerado está salvo na raiz do projeto ou em um caminho acessível.
Deletar as Migrações
Windows
Get-ChildItem -Path .\**\migrations\ -Filter "*.py" -Recurse | Where-Object { $_.Name -ne "__init__.py" } | Remove-Item -Force
Linux e Mac
find . -path "*/migrations/*.py" ! -name "__init__.py" -type f -exec rm -f {} +
Populando o Banco
Use o seguinte comando para rodar o script SQL no linux ou mac:
python manage.py dbshell < script.sql
No windows, no powershell:
Get-Content -Encoding UTF8 script.sql | sqlite3 db.sqlite3
Substitua script.sql
pelo nome e caminho do arquivo SQL que você quer aplicar.
Pronto! Os dados serão inseridos automaticamente no banco, incluindo categorias, produtos, e outras tabelas relacionadas.
Agora podemos testar.