Aula 73 – Loja Online – Atualizando o Carrinho – Ajax
Aula 73 – Loja Online – Atualizando o Carrinho – Ajax
Voltar para página principal do blog
Todas as aulas desse curso
Aula 72 Aula 74
Redes Sociais:
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
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.
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?
Olá Gessé Cazuza.
Obrigado pelo comentário.
Esse carrinho ainda pode ser melhorado mesmo, boa sugestão.
Valeu \o/
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.
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 %}
{% include 'carts/snippets/remove-product.html' with product_id=product.id %}
{% endfor %}
{% 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.