Cómo crear un carrito de compras persistente en Django

Cómo crear un carrito de compras persistente en Django

Introducción al sistema de carrito de compras híbrido con Django

En este artículo te mostraré cómo implementar un sistema de carrito de compras híbrido en un proyecto Django. El objetivo es que tanto usuarios anónimos como autenticados puedan añadir productos al carrito de forma fluida y coherente.

Con este enfoque, un visitante de la web puede comenzar a añadir juegos a su carrito sin necesidad de registrarse. Si posteriormente decide iniciar sesión, los productos que había añadido como anónimo se fusionarán automáticamente con su carrito persistente, sin pérdida de información.

Además, los usuarios autenticados dispondrán de un carrito que se guarda permanentemente en la base de datos, de modo que si cierran sesión o acceden desde otro dispositivo, su carrito seguirá disponible.

Este sistema aporta una excelente experiencia de usuario y es el patrón más habitual en aplicaciones de comercio electrónico modernas.

A lo largo del artículo veremos:

  • Cómo diseñar un modelo de carrito persistente con Django.
  • Cómo adaptar las vistas para funcionar en modo híbrido (sesión o persistente).
  • Cómo usar una señal (user_logged_in) para fusionar automáticamente el carrito de sesión con el persistente.
  • Cómo mantener la interfaz consistente con un context_processor que muestra el número de productos en todo momento.
  • Cómo preparar la aplicación para futuras extensiones (pedidos, cupones, gestión de stock…).

¡Vamos a construirlo paso a paso!

Perfecto, vamos a construir un artículo completo, en varias partes, bien explicado y con todo el código que has ido consolidando.

Los modelos Django

El primer paso para tener persistencia es definir los modelos que nos permitirán guardar los carritos e items en la base de datos.

Hemos creado un modelo Carrito, que es un carrito asociado a un usuario (OneToOne con User, es decir, un carrito por usuario), y un modelo ItemCarrito, que es cada línea del carrito (producto + cantidad).

Aquí está el contenido del models.py de la app carrito:

from django.db import models
from django.contrib.auth.models import User
from juegos.models import Juego

class Carrito(models.Model):
    usuario = models.OneToOneField(User, on_delete=models.CASCADE)
    creado_en = models.DateTimeField(auto_now_add=True)
    actualizado_en = models.DateTimeField(auto_now=True)

    def __str__(self):
        return f"Carrito de {self.usuario.username}"

    def total(self):
        return sum(item.subtotal() for item in self.items.all())

class ItemCarrito(models.Model):
    carrito = models.ForeignKey(Carrito, related_name='items', on_delete=models.CASCADE)
    juego = models.ForeignKey(Juego, on_delete=models.CASCADE)
    cantidad = models.PositiveIntegerField(default=1)

    class Meta:
        unique_together = ('carrito', 'juego')

    def subtotal(self):
        return self.juego.precio * self.cantidad

    def __str__(self):
        return f"{self.cantidad} x {self.juego.nombre}"

El modelo Carrito es muy simple: contiene la relación con el usuario y un par de campos automáticos de fecha. Además, le añadimos un método total() que nos permite calcular el total del carrito fácilmente.

El modelo ItemCarrito relaciona un Carrito con un Juego y contiene un campo cantidad que nos indica cuántas unidades de ese juego hay en el carrito.
Hemos definido una restricción unique_together para que un mismo juego no pueda aparecer más de una vez en un mismo carrito.
El método subtotal() es útil para calcular el subtotal de cada línea (precio del juego × cantidad).


Migraciones de los modelos Django

Una vez definidos los modelos, debemos crear las migraciones correspondientes y aplicarlas a la base de datos.

Esto se hace con los siguientes comandos de Django (debes ejecutarlos en el terminal, estando en el directorio raíz del proyecto):

python manage.py makemigrations carrito
python manage.py migrate carrito

El primer comando genera las migraciones necesarias para crear las tablas Carrito e ItemCarrito.
El segundo comando aplica esas migraciones y crea las tablas en tu base de datos.

Con esto ya tenemos la estructura de datos preparada para soportar carritos persistentes.


Preparando el context processor de la app Django

Queremos que en toda nuestra aplicación podamos mostrar cuántos productos hay en el carrito del usuario.
Para ello, utilizamos un context_processor, que es una función que añade datos al contexto de todas las plantillas.

El contenido de context_processors.py en la app carrito es el siguiente:

from .models import Carrito

def carrito_total_items(request):
    if request.user.is_authenticated:
        carrito = Carrito.objects.filter(usuario=request.user).first()
        total_items = sum(item.cantidad for item in carrito.items.all()) if carrito else 0
    else:
        carrito_sesion = request.session.get('carrito', {})
        total_items = sum(item['cantidad'] for item in carrito_sesion.values())
    return {'carrito_total_items': total_items}

La función carrito_total_items comprueba si el usuario está autenticado.
Si lo está, recuperamos su carrito de base de datos (si existe), y sumamos las cantidades de sus items.
Si no está autenticado, usamos el carrito que tenemos guardado en sesión, exactamente como funcionaba antes.

Esto permite mostrar en la barra superior o en cualquier parte del sitio un indicador con el número total de productos en el carrito, tanto para usuarios logueados como para invitados.

Importante: no olvides registrar este context processor en tu settings.py, en la sección TEMPLATES, dentro de OPTIONS['context_processors'].

Por ejemplo:

'OPTIONS': {
    'context_processors': [
        ...
        'carrito.context_processors.carrito_total_items',
    ],
},

Perfecto, vamos con la siguiente parte del artículo: las vistas.
Te lo voy a explicar bien, paso a paso, para que veas la lógica completa de este nuevo enfoque que combina carrito en sesión + carrito persistente.

Adaptación de las vistas de la app carrito de Django

Las vistas de la app carrito son el núcleo funcional de nuestro sistema.
Teníamos ya una versión previa que usaba solo sesión. Ahora la hemos mejorado para que funcione así:

  • Si el usuario está autenticado → usamos el carrito persistente en base de datos.
  • Si el usuario no está autenticado → seguimos usando la sesión, igual que antes.

Así mantenemos compatibilidad con usuarios anónimos (visitantes), y damos una mejor experiencia a los registrados.

Veamos ahora el contenido completo y actualizado del archivo views.py, explicado por partes.


Importaciones iniciales

Empezamos con las importaciones necesarias:

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from juegos.models import Juego
from .models import Carrito, ItemCarrito

Aquí hemos añadido la importación de nuestros nuevos modelos Carrito e ItemCarrito, ya que los vamos a usar.


Vista para agregar al carrito

Esta vista permite al usuario añadir un juego al carrito.
Aquí es donde diferenciamos claramente si el usuario está logueado o no:

def agregar_al_carrito(request, juego_id):
    juego = get_object_or_404(Juego, pk=juego_id)

    if request.user.is_authenticated:
        # Carrito persistente
        carrito, created = Carrito.objects.get_or_create(usuario=request.user)
        item, created = ItemCarrito.objects.get_or_create(carrito=carrito, juego=juego)
        if not created:
            item.cantidad += 1
            item.save()
    else:
        # Carrito en sesión
        carrito = request.session.get('carrito', {})
        juego_id_str = str(juego.id)
        if juego_id_str in carrito:
            carrito[juego_id_str]['cantidad'] += 1
        else:
            carrito[juego_id_str] = {
                'nombre': juego.nombre,
                'precio': float(juego.precio),
                'cantidad': 1
            }
        request.session['carrito'] = carrito
        request.session.modified = True

    messages.success(request, f"'{juego.nombre}' añadido al carrito.")
    return redirect('juegos.index')

Explicación:

  • Si el usuario está logueado:
    • obtenemos (o creamos) su carrito persistente.
    • obtenemos (o creamos) un ItemCarrito para ese juego.
    • si ya existía, aumentamos su cantidad.
  • Si el usuario no está logueado:
    • usamos el sistema antiguo basado en sesión (request.session).

Resultado: mismo comportamiento que antes, pero ahora con persistencia para usuarios logueados.


Vista para mostrar el carrito

Cuando el usuario entra al carrito queremos mostrar sus productos.
Aquí también diferenciamos si es carrito persistente o en sesión:

def indice_carrito(request):
    juegos = []
    total = 0

    if request.user.is_authenticated:
        carrito, created = Carrito.objects.get_or_create(usuario=request.user)
        items = carrito.items.select_related('juego').all()

        for item in items:
            subtotal = item.subtotal()
            total += subtotal
            juegos.append({
                'id': item.juego.id,
                'nombre': item.juego.nombre,
                'precio': float(item.juego.precio),
                'cantidad': item.cantidad,
                'subtotal': subtotal,
            })

    else:
        carrito = request.session.get('carrito', {})
        for juego_id_str, item in carrito.items():
            subtotal = item['precio'] * item['cantidad']
            total += subtotal
            juegos.append({
                'id': juego_id_str,
                'nombre': item['nombre'],
                'precio': item['precio'],
                'cantidad': item['cantidad'],
                'subtotal': subtotal,
            })

    datos_plantilla = {
        'juegos': juegos,
        'total': total,
        'titulo': 'Carrito de compras'
    }

    plantilla_principal = {
        'title': 'Carrito de compras'
    }

    return render(request, 'carrito/index.html', {
        'datos_plantilla': datos_plantilla,
        'plantilla_principal': plantilla_principal
    })

Explicación:

  • Si el usuario está autenticado:
    • cargamos su carrito persistente.
    • recorremos sus items.
  • Si no está autenticado:
    • mostramos los datos guardados en la sesión.

De nuevo, misma experiencia para el usuario.


Vista para limpiar el carrito

Esta vista permite vaciar el carrito completo:

def limpiar_carrito(request):
    if request.user.is_authenticated:
        carrito = Carrito.objects.filter(usuario=request.user).first()
        if carrito:
            carrito.items.all().delete()
            messages.success(request, "El carrito ha sido vaciado.")
        else:
            messages.info(request, "El carrito ya estaba vacío.")
    else:
        if 'carrito' in request.session:
            del request.session['carrito']
            messages.success(request, "El carrito ha sido vaciado.")
        else:
            messages.info(request, "El carrito ya estaba vacío.")

    return redirect('carrito:indice')

Explicación:

  • Si el usuario está autenticado:
    • borramos todos los items de su carrito en base de datos.
  • Si no está autenticado:
    • eliminamos el carrito de la sesión.

Esto unifica el comportamiento de "vaciar carrito".


Vista para eliminar un producto del carrito

Por último, la vista para eliminar un producto concreto del carrito:

def eliminar(request, juego_id):
    if request.user.is_authenticated:
        carrito = get_object_or_404(Carrito, usuario=request.user)
        try:
            item = ItemCarrito.objects.get(carrito=carrito, juego_id=juego_id)
            item.delete()
            messages.success(request, "Juego eliminado del carrito.")
        except ItemCarrito.DoesNotExist:
            messages.warning(request, "El juego no estaba en el carrito.")
    else:
        carrito = request.session.get('carrito', {})
        juego_id_str = str(juego_id)
        if juego_id_str in carrito:
            del carrito[juego_id_str]
            request.session['carrito'] = carrito
            request.session.modified = True
            messages.success(request, "Juego eliminado del carrito.")
        else:
            messages.warning(request, "El juego no estaba en el carrito.")

    return redirect('carrito:indice')

Explicación:

  • Si el usuario está autenticado:
    • buscamos el item en su carrito persistente y lo eliminamos.
  • Si no está autenticado:
    • eliminamos el juego del carrito en sesión.

Una vez más, todo transparente para el usuario.


Las URLs de la app carrito

No hemos cambiado nada aquí, las rutas siguen igual:

from django.urls import path
from . import views

app_name = 'carrito'

urlpatterns = [
    path('', views.indice_carrito, name='indice'),
    path('agregar/<int:juego_id>/', views.agregar_al_carrito, name='agregar'),
    path('eliminar/<int:juego_id>/', views.eliminar, name='eliminar'),
    path('limpiar/', views.limpiar_carrito, name='limpiar'),
]

Es muy importante que la interfaz de usuario no haya cambiado: seguimos usando las mismas URLs, lo cual hace que el resto de tu proyecto no necesite ninguna adaptación.

Fusión de carritos al iniciar sesión en Django

Como te había contado antes, el comportamiento que queremos es el siguiente:

  • Un usuario navega anónimamente y añade productos al carrito → estos se guardan en la sesión.
  • El usuario decide iniciar sesión.
  • Automáticamente, queremos que los productos que estaban en la sesión se añadan al carrito persistente del usuario (si ya tenía un carrito, los productos nuevos se suman, no se reemplazan).

Este es un flujo muy común en e-commerce y mejora mucho la experiencia de usuario.


¿Cómo se logra esto?

La clave está en escuchar la señal user_logged_in.

Django emite esta señal cada vez que un usuario inicia sesión.
Nosotros podemos "escucharla" y ejecutar código cuando eso ocurre.

Para ello hemos creado un archivo llamado signals.py dentro de la app carrito.
Este archivo contiene el siguiente código:

from django.contrib.auth.signals import user_logged_in
from django.dispatch import receiver
from django.contrib import messages
from .models import Carrito, ItemCarrito
from juegos.models import Juego

Aquí importamos todo lo necesario:

  • la señal user_logged_in,
  • nuestros modelos,
  • el sistema de mensajes de Django para mostrar mensajes en la interfaz.

El receptor de la señal

El núcleo es este decorador @receiver que conecta la señal con nuestra función:

@receiver(user_logged_in)
def fusionar_carrito_sesion_con_db(sender, user, request, **kwargs):
    carrito_sesion = request.session.get('carrito', {})
    if not carrito_sesion:
        return  # No hay nada que fusionar

    carrito_db, created = Carrito.objects.get_or_create(usuario=user)

    for juego_id_str, item_data in carrito_sesion.items():
        juego_id = int(juego_id_str)
        juego = Juego.objects.get(pk=juego_id)

        item_db, created = ItemCarrito.objects.get_or_create(carrito=carrito_db, juego=juego)
        if created:
            item_db.cantidad = item_data['cantidad']
        else:
            item_db.cantidad += item_data['cantidad']
        item_db.save()

    # Vaciar carrito de sesión después de fusionar
    del request.session['carrito']
    request.session.modified = True

    messages.info(request, "Se han añadido los juegos del carrito de invitado a tu carrito de usuario.")

Explicación paso a paso

Ahora te explico con calma lo que hace esta función.

Primero, obtenemos el carrito en sesión con la siguiente línea:

carrito_sesion = request.session.get('carrito', {})

Aquí miramos si el usuario tenía un carrito en sesión (anónimo). Si el carrito en sesión está vacío, no hacemos nada.

A continuación, obtenemos o creamos el carrito persistente:

carrito_db, created = Carrito.objects.get_or_create(usuario=user)

Si el usuario ya tenía un carrito persistente, lo usamos. Si no, se crea uno nuevo automáticamente.

Luego, recorremos los ítems de la sesión y los fusionamos con el carrito persistente. El código que realiza esta operación es el siguiente:

for juego_id_str, item_data in carrito_sesion.items():
    juego_id = int(juego_id_str)
    juego = Juego.objects.get(pk=juego_id)
    item_db, created = ItemCarrito.objects.get_or_create(carrito=carrito_db, juego=juego)
    if created:
        item_db.cantidad = item_data['cantidad']
    else:
        item_db.cantidad += item_data['cantidad']
    item_db.save()

Aquí está el corazón de la fusión. Para cada item del carrito de la sesión, obtenemos el juego correspondiente. Luego, buscamos si ese juego ya estaba en el carrito persistente. Si no estaba, lo creamos con la cantidad que tenía en la sesión. Si ya estaba, le sumamos la cantidad proveniente de la sesión. Finalmente, guardamos los cambios.

Una vez que hemos fusionado todo, limpiamos el carrito en sesión para evitar duplicados. Esto se hace así:

del request.session['carrito']
request.session.modified = True

Por último, mostramos un mensaje al usuario para que sepa que la fusión se ha realizado:

messages.info(request, "Se han añadido los juegos del carrito de invitado a tu carrito de usuario.")

Conectar la señal en apps.py

Para que este código se ejecute al arrancar Django, hemos añadido esta línea en el apps.py:

def ready(self):
    import carrito.signals

Esto asegura que al arrancar Django, se importe el archivo signals.py y nuestra función quede conectada a la señal user_logged_in.

Sin esta línea, el sistema de fusión no funcionaría.


Comportamiento que hemos logrado

Con esta lógica ya conseguimos exactamente lo que querías:

  • Si un usuario navega como anónimo y añade productos → estos se guardan en sesión.
  • Al iniciar sesión, los productos de la sesión se suman a su carrito persistente.
  • Si el carrito persistente ya contenía algún producto, los productos de la sesión se suman (sin perder lo que ya había).
  • Después de la fusión, el carrito de la sesión se vacía para no duplicar.

Beneficios del sistema

  • Experiencia consistente: el usuario nunca pierde lo que tenía en el carrito.
  • Compatibilidad completa: todo el código previo (vistas, plantillas, URLs) sigue funcionando igual.
  • Fácil de mantener: la lógica de la fusión está completamente separada, es fácil de testear y mejorar.
  • Extensible: más adelante podrías añadir reglas más avanzadas (por ejemplo, priorizar precios promocionales, controlar stock, etc.).

Adaptación del context_processor en Django

Recordemos: un context processor en Django es una función que inyecta variables en todas las plantillas de tu sitio web.

En este caso, se usa para mostrar en la cabecera (por ejemplo en un icono de carrito o en el menú) el total de productos que hay actualmente en el carrito del usuario.

El código es este:

from .models import Carrito

def carrito_total_items(request):
    if request.user.is_authenticated:
        carrito = Carrito.objects.filter(usuario=request.user).first()
        total_items = sum(item.cantidad for item in carrito.items.all()) if carrito else 0
    else:
        carrito_sesion = request.session.get('carrito', {})
        total_items = sum(item['cantidad'] for item in carrito_sesion.values())
    return {'carrito_total_items': total_items}

Vamos a desglosarlo con calma.


Flujo para usuarios autenticados

if request.user.is_authenticated:
    carrito = Carrito.objects.filter(usuario=request.user).first()
    total_items = sum(item.cantidad for item in carrito.items.all()) if carrito else 0

Si el usuario ha iniciado sesión:

  • Buscamos su carrito persistente en la base de datos.
  • Si existe → sumamos las cantidades de todos los ItemCarrito que tenga.
  • Si no existe → el total es 0.

Este cálculo es 100 % coherente con el carrito persistente que verá en la vista /carrito/ y que se actualiza al agregar/eliminar productos.


Flujo para usuarios anónimos

else:
    carrito_sesion = request.session.get('carrito', {})
    total_items = sum(item['cantidad'] for item in carrito_sesion.values())

Si el usuario no está autenticado (es un visitante anónimo):

  • El carrito está guardado en request.session['carrito'].
  • Sumamos las cantidades de los productos allí guardados.

Resultado

return {'carrito_total_items': total_items}

Finalmente, devolvemos un diccionario con una clave carrito_total_items que estará disponible en todas las plantillas.

Así puedes hacer algo como:

<a href="{% url 'carrito:indice' %}">
     Carrito ({{ carrito_total_items }})
</a>

Esto funciona siempre:

  • Si el usuario es anónimo → muestra el total del carrito en sesión.
  • Si el usuario ha iniciado sesión → muestra el total del carrito persistente.
  • Si el usuario inicia sesión y se hace la fusión → el contador se actualiza automáticamente gracias a la lógica que vimos en signals.py.

Añadir el contador del carrito en la navegación principal

Para mejorar la experiencia del usuario, es importante que el carrito sea accesible y visible en toda la aplicación. Una forma sencilla de hacerlo es añadir un icono de carrito en la barra de navegación superior con un contador de productos.

Este contador debe funcionar tanto para usuarios anónimos (carrito en sesión) como para usuarios autenticados (carrito persistente en base de datos). Aquí es donde el context_processor que creamos previamente cobra todo su sentido.

Usar el context_processor

El archivo context_processors.py de la app carrito define la función carrito_total_items, que calcula el número total de productos en el carrito. Esta función:

  • Si el usuario está autenticado, cuenta los ItemCarrito del carrito asociado en la base de datos.
  • Si el usuario no ha iniciado sesión, cuenta los items guardados en la variable de sesión request.session['carrito'].

Esta función se carga automáticamente en el contexto de las plantillas gracias a la configuración de Django (recordemos que para que funcione hay que registrarla en TEMPLATESOPTIONScontext_processors en settings.py).

Usar la variable en main.html

En el archivo main.html, que actúa como plantilla base de todo el proyecto, encontramos este fragmento en el menú de navegación, dentro de la lista de enlaces:

<li>
  <a href="{% url 'carrito:indice' %}" class="nav-link px-2 position-relative
    {% if request.path == '/carrito/' %}
      text-danger fw-bold
    {% else %}
      text-body
    {% endif %}">
    <i class="fas fa-shopping-cart"></i> Carrito
    {% if carrito_total_items > 0 %}
    <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
      {{ carrito_total_items }}
    </span>
    {% endif %}
  </a>
</li>

¿Qué hace este código?

  • El enlace muestra el icono de carrito (fas fa-shopping-cart).
  • Si carrito_total_items es mayor que 0, muestra un badge rojo con el número de artículos actuales.
  • Además, el link se resalta en rojo (text-danger fw-bold) si el usuario está navegando en la página del carrito.

Resultado

El resultado es una navegación dinámica:

  • Si un usuario navega por la tienda e introduce productos en el carrito, el contador se actualiza automáticamente.
  • Si el usuario inicia sesión, el contador se sincroniza con su carrito persistente.
  • Si cierra sesión, vuelve a mostrarse el carrito de la sesión anónima.

Esto hace que la experiencia de usuario sea fluida y coherente, sin perder contexto sobre el estado del carrito.


Con este paso, ya hemos cerrado todo el flujo del artículo:

  • Backend con modelo persistente.
  • Signals para la fusión del carrito de sesión.
  • Vistas adaptadas (agregar, eliminar, limpiar, mostrar).
  • Navegación de usuario con contador visible.
  • Context processor que sincroniza sesión y BD en la interfaz.

Beneficio práctico

Con este context_processor, no tienes que preocuparte de en qué "modo" está el carrito (anónimo o persistente).

La cabecera y otras partes del sitio siempre mostrarán el número real de productos en el carrito.


Notas finales y consejos

Con este sistema hemos convertido nuestra app carrito en un componente robusto y flexible:

  • Usuarios anónimos pueden usar el carrito sin necesidad de registrarse.
  • Al iniciar sesión, el contenido del carrito en sesión se fusiona automáticamente con el carrito persistente en la base de datos.
  • El número de productos en el carrito se muestra de forma consistente en toda la web, gracias al context_processor.
  • Toda la lógica de persistencia está encapsulada en los modelos Carrito e ItemCarrito, facilitando su mantenimiento.

Migraciones

Recuerda siempre crear y aplicar las migraciones cuando introduces nuevos modelos o modificas los existentes:

python manage.py makemigrations carrito
python manage.py migrate

Este paso es necesario para que Django cree en la base de datos las tablas correspondientes para Carrito e ItemCarrito.

Consideraciones sobre integridad de datos

Gracias al uso de on_delete=models.CASCADE, no tendrás problemas de integridad:

  • Si se elimina un User, su carrito y sus items se eliminan automáticamente.
  • Si se elimina un Juego, también se eliminarán los ItemCarrito correspondientes.

Esto mantiene la base de datos limpia sin referencias rotas.

Posibles mejoras futuras

Si deseas evolucionar aún más este sistema, aquí tienes algunas ideas:

  • Permitir al usuario modificar la cantidad de cada producto desde la vista del carrito.
  • Añadir comprobación de stock disponible antes de permitir añadir más unidades.
  • Implementar un sistema de pedidos que copie los items del carrito a un nuevo modelo Pedido y vacíe el carrito tras la compra.
  • Añadir un sistema de descuentos o cupones.

Estas funcionalidades se pueden ir construyendo fácilmente sobre la arquitectura actual.

Rendimiento

Actualmente, este carrito es perfectamente eficiente para un sitio de juegos como el que tienes.

Si en el futuro tu proyecto escala mucho (miles de usuarios concurrentes), podrías considerar:

  • Cachear el total de items en el carrito para evitar recalcularlo en cada petición.
  • Revisar que las consultas a la base de datos sean lo más ligeras posible (por ejemplo, usando values_list en el context_processor).

Conclusión

Has conseguido implementar un sistema de carrito híbrido:

  • Sencillo para el usuario final.
  • Coherente en todo el flujo de navegación.
  • Persistente para usuarios registrados.
  • Sin pérdida de información para anónimos que luego se autentican.

Este patrón es muy utilizado en aplicaciones reales de comercio electrónico, ya que ofrece la mejor experiencia posible para el usuario: no se pierde el esfuerzo de llenar el carrito si decide autenticarse después.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Entrada anterior Cómo resaltar el enlace activo en un menú de navegación con Django + Bootstrap