Ponte cómodo y prepárate para sumergirte en la creación del carro de la compra de nuestra tienda en línea. En este capítulo, daremos vida a un widget que se ubicará en la parte superior derecha de nuestra página. Este widget permitirá a los usuarios acceder al carro de la compra de forma rápida y sencilla. Nuestro objetivo es brindar una experiencia de compra fluida, por lo que la aplicación 'carro' que crearemos conservará los artículos seleccionados, tanto mientras los usuarios navegan como cuando regresan más tarde y abren nuevamente el navegador. Podrán añadir, eliminar y gestionar productos de manera intuitiva y conveniente.
Durante este proceso de desarrollo, exploraremos nuevas funcionalidades de Django. Aprenderemos a almacenar y mantener los artículos del carro de la compra, asegurando que los usuarios siempre tengan acceso a su selección. ¡Comencemos!
Construyendo un Carro de la Compra.
Usando sesiones en Django
request.session['foo'] = 'bar'
request.session.get('foo')
del request.session['foo']
Configuración de sesiones
Finalización de la Sesión.
La configuración `SESSION_EXPIRE_AT_BROWSER_CLOSE` te permite elegir entre sesiones que duran tanto como el navegador está abierto o sesiones persistentes. Por defecto, está establecida en `False`, lo que obliga a que la duración de la sesión sea el valor almacenado en la configuración `SESSION_COOKIE_AGE`. Si configuras `SESSION_EXPIRE_AT_BROWSER_CLOSE` en `True`, la sesión expirará cuando el usuario cierre el navegador, y la configuración `SESSION_COOKIE_AGE` no tendrá ningún efecto.
Además, puedes usar el método `set_expiry()` de `request.session` para sobrescribir la duración de la sesión actual. Esto te permite ajustar dinámicamente la duración de la sesión según sea necesario.
Almacenar el Carro de la compra en una sesión.
Necesitas crear una estructura simple que pueda serializarse a JSON para almacenar elementos del carrito en una sesión. El carrito debe incluir los siguientes datos para cada artículo contenido en él:
- El ID de una instancia de Producto.
- La cantidad seleccionada para el producto.
- El precio unitario del producto.
Como los precios de los productos pueden variar, tomemos el enfoque de almacenar el precio del producto junto con el propio producto cuando se añade al carrito. Al hacerlo de esta manera, se utiliza el precio actual del producto cuando los usuarios lo agregan a su carrito, sin importar si el precio del producto cambia posteriormente. Esto significa que el precio que tiene el artículo cuando el cliente lo añade al carrito se mantiene para ese cliente en la sesión hasta que se complete la compra o finalice la sesión.
A continuación, debes construir la funcionalidad para crear carritos de compra y asociarlos con sesiones. Esto tiene que funcionar de la siguiente manera:
- Cuando se necesite un carrito, se comprueba si se ha establecido una clave de sesión personalizada. Si no hay ningún carrito en la sesión, se crea uno nuevo y se guarda en la clave de sesión del carrito.
- Para solicitudes sucesivas, se realiza la misma comprobación y se obtienen los artículos del carrito a partir de la clave de sesión del carrito. Se recuperan los artículos del carrito de la sesión y sus objetos de Producto relacionados de la base de datos.
Edita el archivo settings.py de tu proyecto y añade la siguiente configuración:
CART_SESSION_ID = 'carro'
Esta es la clave que vas a utilizar para almacenar el carrito en la sesión del usuario. Dado que las sesiones de Django se gestionan por visitante, puedes utilizar la misma clave de sesión de carrito para todas las sesiones.
Creemos una aplicación para gestionar los carritos de compra. Abre la terminal y crea una nueva aplicación ejecutando el siguiente comando desde el directorio del proyecto:
venv$ python manage.py startapp Carro
PracticaDjango/PracticaDjango/settings.py
...
# Application definition
INSTALLED_APPS = [
# my applications
'Proyecto_web_app.apps.ProyectoWebAppConfig',
'Servicios.apps.ServiciosConfig',
'Blog.apps.BlogConfig',
'Contacto.apps.ContactoConfig',
'Tienda.apps.TiendaConfig',
'Carro.apps.CarroConfig',
# third party applications
'django_bootstrap5',
# Mapa del Sitio
'django.contrib.sites', # add sites to installed_apps
'django.contrib.sitemaps', # add Django sitemaps to installed app
# PostgreSQL
...
PracticaDjango/PracticaDjango/carro.py
from django.conf import settings from decimal import Decimal from Tienda.models import Producto class Carro: def __init__(self, request) -> None: """ Inicializa el carro. """ self.session = request.session # construimos un carro de la compra para esta sesión. carro = self.session.get(settings.CART_SESSION_ID) if not carro: # save an empty cart in the session carro = self.session[settings.CART_SESSION_ID] = {} # guardamos el carro en la sesión y va a ser un diccionario # con los productos que el usuario va a comprar # la clave es el id del producto y el valor son las características del producto self.carro = carro
Agregar nuevos productos al carro.
PracticaDjango/PracticaDjango/carro.py
class Carro: #... def agregar(self, producto, cantidad=1, override_cantidad=False): # Si el producto no esta en el carro producto_id = str(producto.id) if producto_id not in self.carro: self.carro[producto_id] = { "producto_id": producto.id, "nombre": producto.nombre, "precio": str(producto.precio), "cantidad": 0, "imagen": producto.imagen.url, } # si el producto esta en el carro # solo sumamos la cantidad if override_cantidad: self.carro[producto_id]["cantidad"] = cantidad else: self.carro[producto_id]["cantidad"] += cantidad self.guardar_carro() def guardar_carro(self): self.session.modified = True
Eliminar los productos del Carro.
PracticaDjango/PracticaDjango/carro.py
class Carro: #... def eliminar(self, producto): """ Elimina el producto del carro. """ producto_id = str(producto.id) if producto_id in self.carro: del self.carro[producto_id] self.guardar_carro()
Restar un producto del carrito.
PracticaDjango/PracticaDjango/carro.py
class Carro:
#...
def restar_producto(self, producto):
"""
Resta el producto del carro.
"""
producto_id = str(producto.id)
if producto_id in self.carro:
self.carro[producto_id]["cantidad"] = (
self.carro[producto_id]["cantidad"] - 1
)
if self.carro[producto_id]["cantidad"] < 1:
self.eliminar(producto)
self.guardar_carro()
Limpiar el carro.
PracticaDjango/PracticaDjango/carro.py
class Carro:
#...
def limpiar_carro(self):
self.session["carro"] = {}
self.session.modified = True
Otros métodos auxiliares de la clase Carro.
PracticaDjango/PracticaDjango/carro.py
class Carro: #... def __iter__(self): """ Iterar sobre los artículos en el carrito y obtener los productos. de la base de datos. """ product_ids = self.carro.keys() # Obtener los productos y añadirlos al carro products = Producto.objects.filter(id__in=product_ids) carro = self.carro.copy() for product in products: carro[str(product.id)]["producto"] = product for item in carro.values(): item["precio"] = Decimal(item["precio"]) item["precio_total"] = item["precio"] * item["cantidad"] yield item def __len__(self): """ Cuenta todos los elementos del carro. """ return sum(item["cantidad"] for item in self.carro.values()) def get_total_price(self): return sum( Decimal(item["precio"]) * item["cantidad"] for item in self.carro.values() ) def clear(self): # elimina el carro de la sesión. del self.session[settings.CART_SESSION_ID] self.guardar_carro()
Colocación del carrito a la derecha en la página de la tienda.
PracticaDjango/Tienda/templates/Tienda/tienda.html
...
</div>
<div class="tab-content">
<!--colocamos el carrito en un float en la parte derecha -->
<div style="float:right">
<div>CESTA</div>
{% include "carro/widget.html" %}
</div>
<div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
<h3>Juegos PS4</h3>
<!-- Contenido para juegos PS4 -->
...
PracticaDjango/Tienda/templates/carro/widget.html
{% with total_items=carro|length %}
<a href="{% url 'carro:mostrar' %}">
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="currentColor" class="bi bi-cart-fill"
viewBox="0 0 16 16">
<path
d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</svg>
<span class="badge rounded-pill bg-danger">{{total_items}}</span>
<div>
<span class="badge rounded-pill bg-danger mt-1">{{carro.get_total_price}} €</span>
</div>
</a>
{% endwith %}
Ahora vamos con las vistas.
PracticaDjango/Carro/views.py
from django.shortcuts import render
# Importamos la clase Carro
from .carro import Carro
# Importamos los productos
from Tienda.models import Producto
# cada vez que modifiquemos algo en el carrito tendrá que reflejarse en la
# pagina de la tienda por lo que tendremos que importar el redirect
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required
# Create your views here.
# vista para agregar un producto al carro
@login_required
def agregar_producto(request, producto_id):
carro = Carro(request)
producto = Producto.objects.get(id=producto_id)
# agregamos el producto al carro
carro.agregar(producto=producto)
# redireccionamos a la tienda
return redirect("Tienda:tienda")
# vista para eliminar un producto del carro
def eliminar_producto(request, producto_id):
carro = Carro(request)
producto = Producto.objects.get(id=producto_id)
# eliminamos el producto del carro
carro.eliminar(producto=producto)
# redireccionamos a la tienda
return redirect("Tienda:tienda")
# restar un producto del carro
def restar_producto(request, producto_id):
carro = Carro(request)
producto = Producto.objects.get(id=producto_id)
# restamos el producto del carro
carro.restar_producto(producto=producto)
# redireccionamos a la tienda
return redirect("Tienda:tienda")
# vista para limpiar el carro
def limpiar_carro(request):
carro = Carro(request)
# limpiamos el carro
carro.limpiar_carro()
# redireccionamos a la tienda
return redirect("Tienda:tienda")
# vista para mostrar los productos del carro
def mostrar_carro(request):
productos = Producto.objects.all()
carro = Carro(request)
contexto = {
"productos": productos,
"carro": carro
}
return render(request, "Tienda/carro_detalle.html", contexto)
Agregar el botón "Agregar al Carro" a cada tarjeta.
PracticaDjango/Tienda/templates/Tienda/tienda.html
...
<div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
<h3>Juegos PS4</h3>
<!-- Contenido para juegos PS4 -->
<div class="row g-4">
{% for producto in productos %}
{% if producto.categoria_id == 1 %}
<div class="col-md-3">
<div class="card h-100" style="width:200px">
<img class="card-img-top" src="{{producto.imagen.url}}" alt="Card image">
<div class="card-body">
<h4 class="card-title text-center">{{producto.nombre}}</h4>
<p class="card-text text-center">{{producto.precio}} €</p>
</div>
<div class="card-footer text-center">
<a href="{% url 'carro:agregar' producto.id %}" class="btn btn-success">Agregar al Carro</a>
</div>
</div>
</div>
...
Urls correspondientes a la aplicación carro.
PracticaDjango/Carro/urls.py
from django.urls import path
# load views of these applications.
from . import views
app_name = 'carro'
urlpatterns = [
path('agregar/<int:producto_id>/', views.agregar_producto, name='agregar'),
path('eliminar/<int:producto_id>/', views.eliminar_producto, name='eliminar'),
path('restar/<int:producto_id>/', views.restar_producto, name='restar'),
path('limpiar/', views.limpiar_carro, name='limpiar'),
path('mostrar/', views.mostrar_carro, name='mostrar'),
]
PracticaDjango/PracticaDjango/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from .sitemaps import BlogSitemap, StaticSitemap
sitemaps = {
'static':StaticSitemap, #add StaticSitemap to the dictionary
'blog':BlogSitemap #add DynamicSitemap to the dictionary
}
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('Proyecto_web_app.urls')),
path('servicios/', include('Servicios.urls')),
path('blog/', include('Blog.urls')),
path('contacto/', include('Contacto.urls')),
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
path('tienda/', include('Tienda.urls')),
path('carro/', include('Carro.urls')),
]
urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Lo mas importante context_processor.
Configurando el carrito en el contexto de la solicitud
PracticaDjango/Carro/context_processor.py
from .carro import Carro
def carro(request):
return {'carro': Carro(request)}
PracticaDjango/PracticaDjango/settings.py
...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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',
# para que la variable global este disponible para todo el proyecto
'Carro.context_processor.carro',
],
},
},
]
...
El context processor del carrito se ejecutará cada vez que se renderice una plantilla usando el RequestContext de Django. La variable carro se establecerá en el contexto de tus plantillas. Puedes obtener más información sobre RequestContext en https://docs.djangoproject.com/en/5.0/ref/templates/api/#django.template.RequestContext.
PracticaDjango/Tienda/templates/Tienda/carro_detalle.html
<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
{% load thumbnail %}
<!-- Establecemos el titulo de la página -->
{% block title %}Carro{% endblock %}
<!-- Definimos su contenido -->
{% block content %}
<div class="container">
<div class="bg-white text-black">
{% with total_items=carro|length %}
{% if total_items > 0 %}
<h1>Tu Carro de la Compra</h1>
<p class="ms-3">
<a href="{% url 'carro:limpiar' %}" style="color: #0fa4b8; text-decoration: none;" class="link">
Anular la selección de todos los artículos</a>
</p>
<hr>
</div>
<table class="table table-dark table-striped">
<thead>
<tr>
<th>Imagen</th>
<th>Producto</th>
<th>Cantidad</th>
<th>Eliminar</th>
<th>Precio Unitario</th>
<th>total</th>
</tr>
</thead>
<tbody>
{% for item in carro %}
{% with producto=item.producto %}
<tr>
<td>
<img src="{% thumbnail producto.imagen 100x0 %}">
</td>
<td>{{ producto.nombre }}</td>
<td>{{ item.cantidad }}</td>
<td>
<a href="{% url 'carro:eliminar' producto.id %}" class="btn btn-danger">Eliminar</a>
<a href="{% url 'carro:restar' producto.id %}" class="btn btn-primary">Restar 1 ud</a>
</td>
<td class="num">{{ item.precio }} €</td>
<td class="num">{{item.precio_total}}</td>
</tr>
{% endwith %}
{% endfor %}
<tr class="total">
<td>Total</td>
<td colspan="4"></td>
<td class="num">{{ carro.get_total_price }} €</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-end">
<p class="text-right">
<a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
<a href="#" class="btn btn-success ml-2">Checkout</a>
</p>
</div>
{% else %}
<div class="container py-4">
<div class="bg-white text-black">
<h3 class="ms-3 pt-3">Tu cesta de UnikGAME está vacía.</h3>
<p class="ms-3 pb-3">Revisa tus productos guardados para más tarde o
<a href="{% url 'Tienda:tienda' %}">continúa comprando.</a>
</p>
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% endblock %}
Registrar los pedidos de los clientes.
PracticaDjango/PracticaDjango/settings.py
...
NSTALLED_APPS = [
# Nuestras aplicaciones
'Proyecto_web_app.apps.ProyectoWebAppConfig',
'Servicios.apps.ServiciosConfig',
'Blog.apps.BlogConfig',
'Contacto.apps.ContactoConfig',
'Tienda.apps.TiendaConfig',
'Carro.apps.CarroConfig',
'Orders.apps.OrdersConfig',
# Aplicaciones de terceros
Creando modelos de pedido
PracticaDjango/Orders/models.py
from django.db import models
from Tienda.models import Producto
# Create your models here.
class Order(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField()
address = models.CharField(max_length=250)
postal_code = models.CharField(max_length=20)
city = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
paid = models.BooleanField(default=False)
class Meta:
ordering = ["-created"]
indexes = [
models.Index(fields=["-created"]),
]
def __str__(self):
return f"Orden {self.id}"
def get_total_cost(self):
return sum(item.get_cost() for item in self.items.all())
class OrderItem(models.Model):
order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE)
product = models.ForeignKey(
Producto, related_name="order_items", on_delete=models.CASCADE
)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.PositiveIntegerField(default=1)
def __str__(self):
return str(self.id)
def get_cost(self):
return self.price * self.quantity
Incluyendo los modelos en el panel de administración.
PracticaDjango/Orders/admin.py
from django.contrib import admin
from .models import Order, OrderItem
# Register your models here.
class OrderItemInline(admin.TabularInline):
model = OrderItem
raw_id_fields = ['product']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'first_name', 'last_name', 'email',
'address', 'postal_code', 'city', 'paid',
'created', 'updated']
list_filter = ['paid', 'created', 'updated']
inlines = [OrderItemInline]
Creación de órdenes de clientes
PracticaDjango/Orders/forms.py
from django import forms from .models import Order class OrderCreateForm(forms.ModelForm): class Meta: model = Order fields = ["first_name", "last_name", "email", "address", "postal_code", "city"]
PracticaDjango/Orders/views.py
from django.shortcuts import render from .models import OrderItem from .forms import OrderCreateForm from Carro.carro import Carro # Create your views here. def order_create(request): carro = Carro(request) if request.method == "POST": form = OrderCreateForm(request.POST) if form.is_valid(): order = form.save() for item in carro: OrderItem.objects.create( order=order, product=item["producto"], price=item["precio"], quantity=item["cantidad"], ) # Limpia el carro carro.clear() return render(request, "Orders/order/created.html", {"order": order}) else: form = OrderCreateForm() return render(request, "Orders/order/create.html", {"cart": carro, "form": form})
PracticaDjango/Orders/urls.py
from django.urls import path
from . import views
app_name = 'orders'
urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
PracticaDjango/PracticaDjango/urls.py
urlpatterns = [ #... path('orders/', include('Orders.urls', namespace='orders')), ]
PracticaDjango/Tienda/templates/Tienda/carro_detalle.html
<div class="d-flex justify-content-end">
<p class="text-right">
<a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
<a href="#" class="btn btn-success ml-2">Checkout</a>
</p>
</div>
PracticaDjango/Tienda/templates/Tienda/carro_detalle.html
<div class="d-flex justify-content-end">
<p class="text-right">
<a href="{% url 'Tienda:tienda' %}" class="btn btn-primary">Continua comprando</a>
<a href="{% url 'orders:order_create' %}" class="btn btn-success ml-2">Checkout</a>
</p>
</div>
PracticaDjango/Orders/templates/Orders/order/create.html
{% extends "Proyecto_web_app/base.html" %}
{% block title %}Checkout{% endblock %}
{% block content %}
<div class="custom-container">
<h1>Checkout</h1>
<div class="row">
<div class="col-md-5 text-end">
<div class="card">
<div class="card-body">
<h3>Formulario Pedido</h3>
<form method="post" class="order-form">
{% csrf_token %}
<div class="mb-3">
{{ form.first_name.errors }}
<label for="{{ form.first_name.id_for_label }}">Nombre:</label>
{{ form.first_name }}
</div>
<div class="mb-3">
{{ form.last_name.errors }}
<label for="{{ form.last_name.id_for_label }}">Apellidos:</label>
{{ form.last_name }}
</div>
<div class="mb-3">
{{ form.email.errors }}
<label for="{{ form.email.id_for_label }}">Email:</label>
{{ form.email }}
</div>
<div class="mb-3">
{{ form.address.errors }}
<label for="{{ form.address.id_for_label }}">Dirección:</label>
{{ form.address }}
</div>
<div class="mb-3">
{{ form.postal_code.errors }}
<label for="{{ form.postal_code.id_for_label }}">Código Postal:</label>
{{ form.postal_code }}
</div>
<div class="mb-3">
{{ form.city.errors }}
<label for="{{ form.city.id_for_label }}">Localidad:</label>
{{ form.city }}
</div>
<!-- Agrega los campos restantes del formulario de manera similar -->
<div class="mb-3">
<input type="submit" class="btn btn-primary" value="Realizar Pedido">
</div>
</form>
</div>
</div>
</div>
<div class="col-md-7">
<div class="order-info">
<h3>Your order</h3>
<ul>
{% for item in cart %}
<li>
{{ item.cantidad }}x {{ item.producto.nombre }}
<span>{{ item.precio_total }} €</span>
</li>
{% endfor %}
</ul>
<p>Total: {{ cart.get_total_price }} €</p>
</div>
</div>
</div>
</div>
{% endblock %}
PracticaDjango/Orders/templates/Orders/order/create.html
{% extends "Proyecto_web_app/base.html" %}
{% block title %}Gracias{% endblock %}
{% block content %}
<h1>¡Muchas Gracias!</h1>
<p>Tu pedido se ha completado exitosamente. El número de tu pedido es el
<strong>{{ order.id }}</strong>.</p>
{% endblock %}