Aula 96 – Django – Ecommerce – Refatoração do Projeto

Aula 96 – Django – Ecommerce – Refatoração do Projeto

Loja Online - Django

Loja Online – Django

Voltar para página principal do blog

Todas as aulas desse curso

Aula 95                                   Aula 97

 

Redes Sociais do Código Fluente:

facebook

 

 


Scarlett Finch

Scarlett Finch é uma influenciadora virtual criada com IA.

Ela é 🎤 cantora e 🎶compositora pop britânica.

Siga a Scarlett Finch no Instagram:

facebook

 


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:

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

PIX Nubank

PIX Nubank


 

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 atributo crossorigin="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âmetro slug da URL para buscar o produto correspondente no banco de dados. Essa mudança destaca a busca explícita pelo slug, que é um identificador único para cada produto.
  • A verificação if self.request.user.is_authenticated com ObjectViewed.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 era ProductDetailSlugView. Agora, foi substituída por ProductDetailView, 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 view ProductListView 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 por ProductDetailView: A rota para detalhes de produtos (<slug:slug>/) agora utiliza a nova view ProductDetailView. 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 (uma FormView) 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' %} ou reverse('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 modelo Product, 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 e featured (list_filter).
    • Campos de pesquisa para title e description (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).

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.
  • 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.
  • 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.
  • 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 atributo name='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.
  • 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.

Fluxo de trabalho

  1. Usuário começa a digitar no campo de busca.
  2. Após 0,5 segundos sem digitar, o temporizador chama a função performSearch.
  3. O botão de busca é desabilitado e o ícone de carregamento aparece.
  4. 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, como map().
  • O toastElList.map(function(toastEl) {...}) itera sobre cada item na lista e executa uma função que cria uma nova instância de bootstrap.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 Novase_commerce/e_commerce/settings.py

  • Configuração de variáveis de ambiente essenciais: SECRET_KEY, DEBUG e ALLOWED_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 em INSTALLED_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 e HttpOnly) 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 para None, 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 como False 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 para False 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 modelo User 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, e create_superuser) com valor padrão como False.
  • Ajustes nos métodos de criação: Os métodos de criação (create_user, create_staffuser, e create_superuser) foram atualizados para incluir o campo is_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 da LoginView, autentica o usuário, faz login e envia um sinal personalizado user_logged_in. Remove qualquer guest_email_id da sessão e redireciona para success_url.
  • Exibe uma mensagem de erro com messages.error caso as credenciais de login sejam inválidas.
  • No método get da LogoutView, 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 na RegisterView para redirecionar para a página de login após o registro bem-sucedido.
  • No método form_valid da RegisterView, salva o usuário com a senha definida e exibe uma mensagem de sucesso com messages.success após o registro.
  • No método form_invalid da RegisterView, exibe mensagens de erro detalhadas para cada campo do formulário usando messages.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ção guest_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 modelo Category, permitindo associar produtos a categorias, melhorando a organização e filtragem de produtos.
  • Ajuste no search do QuerySet:
    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) e order (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

  • Adição do campo para especificar quantidade: Um campo <input> foi adicionado para que o usuário possa selecionar a quantidade de produtos, com as propriedades:
    • type="number": Permite entrada numérica.
    • value="1": Define o valor inicial como 1.
    • min="1" e max="{{ product.stock }}": Define limites para a quantidade, garantindo que ela esteja disponível no estoque.
    • class="form-control form-control-sm text-center": Aplica estilos para exibição responsiva e alinhamento centralizado.
    • style="width: 80px;": Limita a largura do campo.
  • Reformulação do botão “Adicionar”:
    • Mantém o texto “Adicionar”, mas foi redesenhado com a classe btn-primary e um tamanho menor (btn-sm).
    • O botão agora está alinhado ao lado do campo de quantidade graças à nova estrutura.

Essas alterações melhoraram a usabilidade, adicionaram controle de quantidade diretamente no formulário e organizaram os elementos de forma mais clara e responsiva.

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 usando object.image e exibia essa imagem, se presente. Agora, ele verifica se há múltiplas imagens relacionadas ao produto com object.images.all e usa um loop for 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 atributo alt para acessibilidade e SEO. Ele utiliza o campo alt_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 de 1 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 com csrf_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 e row justify-content-center para centralização horizontal do conteúdo. A classe col-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 por card-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 como alert 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ção initialize() 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âmetro payment_intent_client_secret. Dependendo do status (succeeded, processing, ou requires_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 e del 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&oacute;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 classe display-4 para maior destaque e estilo. A descrição agora enfatiza a experiência de compras online, com classe lead para melhorar a tipografia.
  • Atualização da imagem principal:
    O nome do arquivo de imagem foi alterado para eCommerce.jpg, e a imagem agora ocupa 100% da largura (w-100) com altura limitada a 600px, usando object-fit: cover para manter proporções visuais.
  • Alteração na estrutura e espaçamento:
    Foram adicionadas classes de espaçamento como mb-4 e mb-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 Bootstrap container e mt-5 para estruturar o conteúdo.
  • Exibe mensagens de feedback (messages) em um componente toast do Bootstrap, com classes e estilos dinâmicos baseados nos tags das mensagens (success, error, warning, etc.).
  • Estrutura o formulário de login dentro de uma div centralizada com classes como row justify-content-center e card shadow-sm para estilização e layout.
  • Exibe o título “Login” com classes Bootstrap para estilização, como text-center, mb-4, e text-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 como form-control form-control-lg aos campos email e password do formulário.
  • Adiciona uma opção “Lembrar senha” com um checkbox estilizado usando classes Bootstrap form-check-input e form-check-label.
  • Inclui um campo oculto para o parâmetro next, que preserva o redirecionamento após o login, baseado no valor de request.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 Bootstrap form-control e form-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 caso action_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" }}.
  • 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 entre Cart e Product. Isso permite adicionar o campo quantity para rastrear quantidades individuais de produtos no carrinho.
  • Alteração no relacionamento products no modelo Cart:
    O campo products agora utiliza o modelo intermediário CartProduct, permitindo associar informações extras, como quantidade, a cada produto no carrinho.
  • Método update_totals no modelo Cart:
    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étodo update_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 de template.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 argumento arg) fornecidos no template.
      • Tratamento de erros: Se os valores não puderem ser convertidos para float ou forem inválidos, retorna 0.
      • Uso em templates: {{ value|multiply:arg }} para multiplicar value por arg.
    • 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 atributo class com o valor fornecido como css_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 classe form-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.

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 view add_to_cart com o nome add_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.

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 por street.
  • Remoção de address_line_2 e inclusão de novos campos: complement, neighborhood, e number.
  • Adição de default="" em campos obrigatórios como street, city, state, e postal_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

  • print("========= DEBUG INFO ========="): Exibe informações de depuração no console para verificar o comportamento do código durante a execução.
  • print("Method:", request.method): Imprime o método HTTP usado na requisição (por exemplo, “POST”).
  • print("POST data:", request.POST): Exibe os dados enviados no corpo da requisição POST.
  • print("Address Type:", request.POST.get('address_type')): Mostra o tipo de endereço enviado no formulário, como “billing” ou “shipping.”
  • print("Form is valid:", form.is_valid()): Indica se o formulário enviado é válido.
  • if billing_profile is not None:: Verifica se o perfil de faturamento foi encontrado ou criado.
  • print("Address type after validation:", address_type): Exibe o tipo de endereço após a validação dos dados do formulário.
  • if address_type == "billing":: Condicional para redirecionar o usuário para o método de pagamento se o tipo de endereço for “billing.”
  • print("Redirecting to payment..."): Mensagem de depuração indicando que o redirecionamento para pagamento está ocorrendo.

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 modelo Address 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” para street, “Número” para number, e assim por diante.
  • widgets: Aplica widgets personalizados aos campos para estilização, usando a classe CSS form-control para cada campo, garantindo consistência visual no formulário.
  • def clean_street(self):: Método de validação para o campo street, 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 campo city, 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 campo postal_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 como POST e configura a URL de ação. Se action_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 para next_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 de next_url, usado para redirecionar o usuário após o login.
  • {% if address_type %}: Verifica se address_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 CSS btn 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 set address_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étodo POST e define a URL de ação como o valor de action_url.
  • {% if next_url %}: Verifica se existe um valor para next_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 de next_url, usado para redirecionar o usuário após o envio do formulário.
  • {% if address_type %}: Verifica se há um valor para address_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 set address_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á o id 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 Bootstrap btn 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.

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>