Aula 73 – Loja Online – Atualizando o Carrinho – Ajax

Aula 73 – Loja Online – Atualizando o Carrinho – Ajax

Loja Online - Django

Loja Online – Django

Pacote Programador Fullstack

Pacote Programador Fullstack

Voltar para página principal do blog

Todas as aulas desse curso

Aula 72                       Aula 74

Redes Sociais:

facebook              

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Melhore seu NETWORKING

https://digitalinnovation.one/

Participe de comunidades de desenvolvedores:

Fiquem a vontade para me adicionar ao linkedin.

E também para me seguir no https://github.com/toticavalcanti.

Código final da aula:

https://github.com/toticavalcanti

Quer aprender python3 de graça e com certificado? Acesse então:

https://workover.com.br/python-codigo-fluente

Canais do Youtube

Toti:

Toti

Backing Track / Play-Along

Código Fluente

Putz!

PIX para doações

PIX Nubank

PIX Nubank


Aula 73 – Loja Online – Atualizando o Carrinho – Ajax

Antes de seguir, uma observação.

Na release 3.2 do Django, ao definir um modelo, se nenhum campo em um modelo for definido com primary_key=True, uma chave primária implícita é adicionada.

O tipo dessa chave primária implícita agora pode ser controlado por meio da DEFAULT_AUTO_FIELD.

Não há mais necessidade de substituir chaves primárias em todos os modelos.

Mantendo o comportamento histórico, o valor padrão para DEFAULT_AUTO_FIELD é AutoField.

A partir do 3.2, novos projetos são gerados com DEFAULT_AUTO_FIELD como BigAutoField.

Além disso, novos aplicativos são gerados com AppConfig.default_auto_field definido como BigAutoField.

Em uma versão futura do Django, o valor padrão de DEFAULT_AUTO_FIELD será alterado para BigAutoField.

Então vamos definir assim: DEFAULT_AUTO_FIELD = “django.db.models.BigAutoField”

Vamos colocar o que tá em azul no settings.py, para evitar esses WARNINGS:

WARNINGS:
accounts.GuestEmail: (models.W042) Auto-created primary key used when not defining a primary key type, by default ‘django.db.models.AutoField’.
HINT: Configure the DEFAULT_AUTO_FIELD setting or the AccountsConfig.default_auto_field attribute to point to a subclass of AutoField, e.g. ‘django.db.models.BigAutoField’.
addresses.Address: (models.W042) Auto-created primary key used when not defining a primary key type, by default ‘django.db.models.AutoField’.

django_ecommerce\e_commerce\e_commerce\settings.py 


"""
Django settings for e_commerce project.

Generated by 'django-admin startproject' using Django 2.1.4.

For more information on this file, see
https://docs.djangoproject.com/en/2.1/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'xjmv-0^l__duq4-xp54m94bsf02lx4&1xka_ykd_(7(5#9^1o^'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    #our apps
    'addresses',
    'billing',
    'accounts',
    'carts',
    'orders',
    'products',
    'search',
    'tags',
]

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

LOGOUT_REDIRECT_URL = '/login/'
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'


# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators

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',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

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")

Agora execute o makemigrations e o migrate.


python manage.py makemigrations

python manage.py migrate

Seguindo

Como no futuro vamos usar diferentes endpoints, vamos colocar o parâmetro data-endpoint no update-cart.html.

Vamos substituir a linha
const actionEndpoint = thisForm.attr(“action“);

Por:
const actionEndpoint = thisForm.attr(“data-endpoint“);

E acrescentar o atributo data-endpoint no form do update-cart.html.

Uso do  data-endpoint

  • data-endpoint=”relative_url”
  • data-endpoint=”/absolute_url”
  • data-endpoint=”http://full_url”

Exemplos

  • data-endpoint=”world.php”
  • data-endpoint=”/hello/world.php”
  • data-endpoint=”http://www.example.com/hello/world.php”

data-endpoint 

O data-endpoint é usado para especificar um endpoint não padrão de uma API, para recuperar a definição do formulário e enviar os dados resultantes.

Se você estiver usando data-id e sua instância de banco de dados for acessada por uma URL diferente de www2.mysite.com, provavelmente será necessário usar esse atributo.

Padrão: https://api.mysite.com

Exemplo não padrão, para carregar a definição e envio para: https://outraurl.local/


<div class="ngp-form" data-id="12345" data-endpoint="https://outraurl.local">
</div>

e_commerce/products/templates/products/snippets/update-cart.html


<form class='form-product-ajax' method='POST' action='{% url "cart:update" %}' 
  data-endpoint='{% url "cart:update" %}' 
  class="form"> {% csrf_token %}
  <input type='hidden' name='product_id' value='{{ product.id }}' />
  <span class='submit-span'>
    {% if product in cart.products.all %}
      No carrinho <button type='submit' class='btn btn-link'>Excluir</button>
    {% else %}
      <button type='submit' class='btn btn-success'>Adicionar</button>
    {% endif %}
  </span>
</form>

Crie o e_commerce/carts/templates/carts/snippets/remove-product.html

E coloque o seguinte conteúdo nele:

e_commerce/carts/templates/carts/snippets/remove-product.html


<form class='form-product-ajax' method='POST' action='{% url "cart:update" %}' 
  data-endpoint='{% url "cart:update" %}' 
  class="form"> {% csrf_token %}
  <input class="cart-item-product-id" type='hidden' name='product_id' value='{{ product.id }}' />
  <button type='submit' class='btn btn-link btn-sm' style="padding:0px;cursor: pointer;">
    Excluir
  </button>
</form>

Com todas as alterações dessa aula, o base.html vai ficar assim:

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='Loja virtual' %}
    <div class='container'>
      {% block content %} {% endblock %}
    </div>
    {% include 'base/js.html' %}
    <script>
      $(document).ready(function(){
        const productForm = $(".form-product-ajax")
        productForm.submit(function(event){
        event.preventDefault();
        // console.log("O formulário não foi enviado!");
        // o this pega os dados relacionados a esse form
        const thisForm = $(this);
        //const actionEndpoint = thisForm.attr("action");
        const actionEndpoint = thisForm.attr("data-endpoint");
        const httpMethod = thisForm.attr("method");
        const formData = thisForm.serialize();
        $.ajax({
          url: actionEndpoint,
          method: httpMethod,
          data: formData,
          success: function(data){
            // console.log("Sucesso")
            // console.log(data)
            // console.log("Adicionado", data.added)
            // console.log("Removido", data.removed)
            const submitSpan = thisForm.find(".submit-span")
            if(data.added){
              submitSpan.html("No carrinho <button type='submit' class='btn btn-link'>Excluir</button>")
            } else {
              submitSpan.html("<button type='submit' class='btn btn-success'>Adicionar</button>")
            }
            const navbarCount = $(".navbar-cart-count")
            navbarCount.text(data.cartItemCount)
            const currentPath = window.location.href
            if(currentPath.indexOf("cart") != -1){
              refreshCart()
            }
          },
          error: function(errorData){
            console.log("Erro")
            console.log(errorData)
          }
        })
      })
      function refreshCart(){
        //console.log("Excluído do carrinho atual!")
        const cartTable = $(".cart-table")
        const cartBody = cartTable.find(".cart-body")
        //cartBody.html("<h1>Mudou!</h1>")
        const productsRow = cartBody.find(".cart-product")
        const currentUrl = window.location.href 
        const refreshCartUrl = '/api/cart/';
        const refreshCartMethod = "GET";
        const data = {};
        $.ajax({
          url: refreshCartUrl,
          method: refreshCartMethod,
          data: data,
          success: function(data){
            console.log(data)
            const hiddenCartItemRemoveForm = $(".cart-item-remove-form")
            if(data.products.length > 0){
              productsRow.html(" ")
              let i = data.products.length
              $.each(data.products, function(index, value){
                const newCartItemRemove = hiddenCartItemRemoveForm.clone()
                newCartItemRemove.css("display", "block")
                newCartItemRemove.find(".cart-item-product-id").val(value.id)
                cartBody.prepend("<tr><th scope=\"row\">" + i + 
                "</th><td><a href='" + value.url + "'>" + 
                value.name + "</a>" + newCartItemRemove.html() + "</td><td>" + value.price + "</td></tr>")
                i--
              })
              cartBody.find(".cart-subtotal").text(data.subtotal)
              cartBody.find(".cart-total").text(data.total)
            } else {
              window.location.href = currentUrl
              }
            },
            error: function(errorData){
              console.log("Erro")
              console.log(errorData)
            }
          })
        }
      })
    </script>
  </body>
</html>

Para fazer a atualização do carrinho na tela do browser, vamos definir duas classes que vão permitir atualizar o carrinho.

e_commerce/carts/templates/carts/home.html 


{% extends "base.html" %}
{% block content %}
  <h1>Cart</h1>
  {% if cart.products.exists %}
  <table class="table cart-table">
    <thead>
      <tr>
        <th>#</th>
        <th>Nome</th>
        <th>Preço</th>
      </tr>
    </thead>
    <tbody class="cart-body">
       {% for product in cart.products.all %}
        <tr class="cart-product">
          <th scope="row">{{ forloop.counter }}</th>
          <td>
            <a href='{{ product.get_absolute_url }}'>{{ product.title }}</a>
            {% include 'carts/snippets/remove-product.html' with product_id=product.id %}
          </td>
          <td>{{ product.price }}</td>
        </tr>
        {% endfor %}
      <tr>
        <td colspan="2"></td>
        <td><b>Subtotal</b> 
lt;span clas="cart-subtotal"> {{ cart.subtotal }} </span> </td> </tr> <tr> <td colspan="2"></td> <td><b>Total</b> $<span clas="cart-total"> {{ cart.total }} </span> </td> </tr> <tr> <td colspan="2"></td> <td><a class='btn btn-lg btn-success' href='{% url "cart:checkout" %}'>Conferir compra</a></td> </tr> </tbody> </table> <div class="cart-item-remove-form" class='cart-item-remove-form' style='display:none'>  {% include 'carts/snippets/remove-product.html' %}  </div> {% else %} <p class='lead'>Carrinho vazio</p> {% endif %} {% endblock %} 

Vamos criar uma view para lidar com a url /api/cart/ no backend.

Como o cart_obj.products.all() retorna algo como: [<object>, <object>, <object>…] 

O Json não sabe como lidar com os items dessa forma ( [<object>, <object>, <object>…] ), o Django sabe, mas, o JQuery não.

JQuery foi feito para entender Json, ou seja, um dicionário chave valor.

Por isso no código abaixo a gente faz o seguinte:
products = [{“name”: x.title, “price”: x.price} for x in cart_obj.products.all()]
cart_data = {“products”: products, “subtotal”: cart_obj.subtotal, “total”: cart_obj.total}

e_commerce/carts/views.py


from django.http import JsonResponse
from django.shortcuts import render, redirect

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

def is_ajax(request):
    return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'

def cart_detail_api_view(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    products = [{
        "id": x.id,
        "url": x.get_absolute_url(), 
        "name": x.title, 
        "price": x.price
        } for x in cart_obj.products.all()]
    # products_list = []
    # for x in cart_obj.products.all():
    #     products_list.append({
    #         {"name": x.title, "price": x.price}
    #     })
    cart_data = {"products": products, "subtotal": cart_obj.subtotal, "total": cart_obj.total}
    return JsonResponse(cart_data)

def cart_home(request):
    cart_obj, new_obj = Cart.objects.new_or_get(request)
    return render(request, "carts/home.html", {"cart": cart_obj})

def cart_update(request):
    product_id = request.POST.get('product_id')
    if product_id is not None:
        try:
            product_obj = Product.objects.get(id=product_id)
        except Product.DoesNotExist:
            print("Mostrar mensagem ao usuário, esse produto acabou!")
            return redirect("cart:home")
        cart_obj, new_obj = Cart.objects.new_or_get(request)
        if product_obj in cart_obj.products.all():
            cart_obj.products.remove(product_obj)
            added = False
        else:
            cart_obj.products.add(product_obj) # cart_obj.products.add(product_id)
            added = True
        request.session['cart_items'] = cart_obj.products.count()
        # return redirect(product_obj.get_absolute_url())
        if is_ajax(request):
            print("Ajax request")
            json_data = {
                "added": added,
                "removed": not added,
                "cartItemCount": cart_obj.products.count()
            }
            return JsonResponse(json_data)
    return redirect("cart:home")

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", {})

E no urls.py do e_commerce, vamos colocar a url do cart.

django_ecommerce/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 carts.views import cart_home, cart_detail_api_view
from accounts.views import login_page, register_page, logout_page, guest_register_view
from addresses.views import checkout_address_create_view, checkout_address_reuse_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('api/cart/', cart_detail_api_view, name='api-cart')
    path('login/', login_page, name='login'),
    path('register/guest/', guest_register_view, name='guest_register'),
    path('logout/', LogoutView.as_view(), name='logout'),
    path('register/', register_page, name='register'),
    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)

Execute o servidor e teste.

Ficamos por aqui e até a próxima. 😉

Voltar para página principal do blog

Todas as aulas desse curso

Aula 72                       Aula 74

Código final da aula:

https://github.com/toticavalcanti

Canais do Youtube

Toti:

Toti

Backing Track / Play-Along

Código Fluente

Putz!

Dêem um joinha 👍 na página do Código Fluente no
Facebook.

Sigam o Código Fluente no Instagram e no TikTok.

Código Fluente no Pinterest.

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Nos vemos na próxima então, \o/  😉 Bons Estudos!

About The Author
-

4 Comentários

  • Gessé Cazuza
    Reply

    Olá Cavalcanti! Tudo bem?

    Primeiro quero te parabenizar pela atitude! Excelente conteúdo para quem tá iniciando.
    2. Segundo queria te sugerir uma adição nestas aulas com ajax: como adicionar mais de um item? Até agora só é permitido um já que automaticamente seria o REMOVER se já tem um. Poderia dá um exemplo?

    • toticavalcanti
      Reply

      Olá Gessé Cazuza.
      Obrigado pelo comentário.
      Esse carrinho ainda pode ser melhorado mesmo, boa sugestão.
      Valeu \o/

  • GESSÉ
    Reply

    Olá Toti! Tudo bem?
    Estou acompanhando o curso de perto, mas não consigo acrescentar a função para as quantidades no carts/home.html. Tentei fazer o cálculo dos totais (quantidade * price), mas também não rolou na view cart_update. A ideia é acrescentar um botão atualizar carrinho e pegar a nova quantidade inserida ou até fazer direto via ajax no ecommerce.js. Poderia dá um exemplo dessa modificação?

    Att.

    • toticavalcanti
      Reply

      Opa Gesse,
      Tenta algo do tipo:
      No django_ecommerce/e_commerce/carts/templates/carts/home.html


      {% extends "base.html" %}
      {% block content %}

      Cart

      {% if cart.products.exists %}


      {% for product in cart.products.all %}

      {% endfor %}

      #NomePreçoQuantidadeAtualizar
      {{ forloop.counter }} {{ product.title }}
      {% include 'carts/snippets/remove-product.html' with product_id=product.id %}
      {{ product.price }}
      Subtotal $ {{ cart.subtotal }}
      Total $ {{ cart.total }}
      Conferir compra

      {% else %}

      Carrinho vazio

      {% endif %}
      {% endblock %}

      django_ecommerce/e_commerce/carts
      /views.py

      ...
      def cart_update(request):
      product_id = request.POST.get('product_id')
      quantity = request.POST.get('quantity', 1) # Adicionado campo de quantidade com padrão 1
      ...
      if product_obj in cart_obj.products.all():
      if quantity <= 0: cart_obj.products.remove(product_obj) else: cart_item = cart_obj.cartitem_set.get(product=product_obj) # Supondo que você tem um modelo CartItem cart_item.quantity = quantity cart_item.save() added = False else: cart_obj.products.add(product_obj) added = True ...

      E no ajax algo do tipo:
      function updateCart(productId){
      var quantity = document.getElementById('quantity-' + productId).value;
      $.ajax({
      type: "POST",
      url: "/caminho/para/cart_update/",
      data: {
      'product_id': productId,
      'quantity': quantity,
      'csrfmiddlewaretoken': '{{ csrf_token }}'
      },
      success: function(data){
      // Atualize a interface do usuário conforme necessário
      }
      });
      }

      Vlw abs.

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>