jueves, 29 de junio de 2023

19.- Creación de la aplicación Cesta de la Compra (variables permanentes o context_processor)

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.


Después de construir el catálogo de productos, el siguiente paso es crear un carrito de compras para que los usuarios puedan seleccionar los productos que desean comprar. Un carrito de compras permite a los usuarios elegir productos y establecer la cantidad que desean ordenar, almacenando esta información temporalmente mientras navegan por el sitio, hasta que finalmente realicen un pedido. El carrito debe persistir en la sesión para que los elementos del carrito se mantengan durante la visita de un usuario.

Utilizarás el framework de sesiones de Django para persistir el carrito. El carrito se mantendrá en la sesión hasta que se complete o el usuario realice el pago. También necesitarás crear modelos adicionales de Django para el carrito y sus elementos.


Usando sesiones en Django


Django proporciona un framework de sesiones que soporta sesiones anónimas y de usuario. El framework de sesiones te permite almacenar datos arbitrarios para cada visitante. Los datos de sesión se almacenan en el servidor y las cookies contienen el ID de sesión a menos que uses el motor de sesión basado en cookies. El middleware de sesión gestiona el envío y la recepción de cookies. El motor de sesión predeterminado almacena los datos de sesión en la base de datos, pero puedes elegir otros motores de sesión.

Para usar sesiones, debes asegurarte de que la configuración MIDDLEWARE de tu proyecto contenga 'django.contrib.sessions.middleware.SessionMiddleware'. Este middleware gestiona las sesiones. Se agrega por defecto a la configuración MIDDLEWARE cuando creas un nuevo proyecto usando el comando startproject.

El middleware de sesión pone la sesión actual a disposición en el objeto de solicitud (request). Puedes acceder a la sesión actual usando request.session, tratándola como un diccionario de Python para almacenar y recuperar datos de sesión. El diccionario de sesión acepta cualquier objeto de Python por defecto que pueda serializarse a JSON. Puedes establecer una variable en la sesión de esta manera:

request.session['foo'] = 'bar'
Para recuperar una clave de sesión lo puedes hacer de la siguiente manera:

request.session.get('foo')
Eliminar una clave que previamente habías guardado en la sesión de usuario se hace así:

del request.session['foo']
Es decir, se trata igual que cualquier diccionario de Python.

Cuando los usuarios inician sesión en el sitio, su sesión anónima se pierde y se crea una nueva sesión para los usuarios autenticados. Si has almacenado elementos en una sesión anónima que necesitas conservar después de que el usuario inicie sesión, deberás copiar los datos de la sesión antigua a la nueva sesión. Puedes hacer esto recuperando los datos de sesión antes de iniciar sesión del usuario utilizando la función login() del sistema de autenticación de Django y almacenándolos en la sesión después de eso.

Configuración de sesiones


Existen varias configuraciones que puedes utilizar para configurar las sesiones de tu proyecto. La más importante es SESSION_ENGINE. Esta configuración te permite establecer el lugar donde se almacenan las sesiones. Por defecto, Django almacena las sesiones en la base de datos utilizando el modelo Session de la aplicación django.contrib.sessions.

Django ofrece las siguientes opciones para almacenar datos de sesión:

- Sesiones en la base de datos: Los datos de sesión se almacenan en la base de datos. Este es el motor de sesión predeterminado.

- Sesiones basadas en archivos: Los datos de sesión se almacenan en el sistema de archivos.

- Sesiones en caché: Los datos de sesión se almacenan en un almacén de caché. Puedes especificar los almacenes de caché utilizando la configuración CACHES. Almacenar datos de sesión en un sistema de caché proporciona el mejor rendimiento.

- Sesiones en caché con base de datos: Los datos de sesión se almacenan en una caché de escritura y en la base de datos. Las lecturas solo utilizan la base de datos si los datos no están ya en la caché.

- Sesiones basadas en cookies: Los datos de sesión se almacenan en las cookies que se envían al navegador.

Para mejorar el rendimiento, se recomienda utilizar un motor de sesión basado en caché. Django admite Memcached de manera nativa, y puedes encontrar almacenes de caché de terceros para Redis y otros sistemas de caché. Utilizar un sistema de caché como Memcached o Redis puede mejorar significativamente el rendimiento de las sesiones en tu aplicación.

Puedes personalizar las sesiones con configuraciones específicas. Aquí hay algunas de las configuraciones importantes relacionadas con las sesiones:

- `SESSION_COOKIE_AGE`: La duración de las cookies de sesión en segundos. El valor predeterminado es 1209600 (dos semanas).

- `SESSION_COOKIE_DOMAIN`: El dominio utilizado para las cookies de sesión. Establece esto en mydomain.com para habilitar cookies entre dominios o usa None para una cookie de dominio estándar.

- `SESSION_COOKIE_HTTPONLY`: Indica si se debe utilizar la bandera HttpOnly en la cookie de sesión. Si se establece en True, JavaScript del lado del cliente no podrá acceder a la cookie de sesión. El valor predeterminado es True para aumentar la seguridad contra el secuestro de sesiones de usuario.

- `SESSION_COOKIE_SECURE`: Un booleano que indica que la cookie solo debe enviarse si la conexión es HTTPS. El valor predeterminado es False.

- `SESSION_EXPIRE_AT_BROWSER_CLOSE`: Un booleano que indica que la sesión debe expirar cuando se cierra el navegador. El valor predeterminado es False.

- `SESSION_SAVE_EVERY_REQUEST`: Un booleano que, si es True, guardará la sesión en la base de datos en cada solicitud. La expiración de la sesión también se actualiza cada vez que se guarda. El valor predeterminado es False.

Puedes ver todas las configuraciones de sesión y sus valores predeterminados en https://docs.djangoproject.com/en/5.0/ref/settings/#sessions.


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

Para que no se nos olvide, la registramos dentro del proyecto:


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
...

Y ahora ya si, comencemos a construir el carro. Tenemos que crear una clase nueva que sea la que maneje lo que se supone que debe poder hacerse en un carro. Para ello dentro del directorio de la aplicación "Carro" creamos el archivo carro.py. 


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    
Esta es la clase Carro que te permitirá gestionar el carrito de compras. Requiere que el carrito sea inicializado con un objeto de solicitud. Almacenas la sesión actual usando self.session = request.session para hacerla accesible a los otros métodos de la clase Carro.

Primero, intentas obtener el carrito de la sesión actual usando self.session.get(settings.CART_SESSION_ID). Si no hay un carrito presente en la sesión, creas un carrito vacío estableciendo un diccionario vacío en la sesión.

Construirás tu diccionario del carrito con IDs de productos como claves, y para cada clave de producto, un diccionario será un valor que incluya cantidad, precio y otros atributos. Al hacer esto, puedes garantizar que un producto no se añada más de una vez al carrito. De esta manera, también puedes simplificar la recuperación de elementos del carrito.

Aquí vamos a hacer un inciso para comentar el objeto "request". En el contexto de Django, un objeto request representa la solicitud que se realiza a una vista o función de vista. Contiene información sobre la solicitud HTTP realizada por el cliente, como los parámetros enviados, las cookies, la información del usuario, el método HTTP utilizado, entre otros datos.

El objeto request se pasa automáticamente a las vistas de Django cuando se reciben solicitudes HTTP. Al definir una función de vista o una clase de vista en Django, se puede incluir un parámetro llamado request para recibir este objeto. Django se encarga de instanciar y pasar el objeto request automáticamente cuando se llama a la vista.

Se puede decir que el objeto request en Django comparte algunas similitudes con un diccionario de Python. El objeto request contiene información relacionada con la solicitud HTTP, y se puede acceder a los datos de la misma manera que se accede a los elementos de un diccionario.

El objeto request en Django proporciona métodos y atributos que permiten acceder a diferentes aspectos de la solicitud, como los parámetros GET y POST, las cookies, las cabeceras, la información del usuario autenticado y más. Estos datos se pueden acceder utilizando la sintaxis de diccionario de Python.

Por ejemplo, para acceder a los parámetros GET de una solicitud, se puede utilizar:

request.GET['nombre_parametro']

Para acceder a los datos POST, se puede usar:

request.POST['nombre_parametro']

Asimismo, las cookies se pueden acceder mediante:

request.COOKIES['nombre_cookie']

Además, el objeto request también permite asignar valores, al igual que un diccionario. Por ejemplo, request.session['clave'] = valor se utiliza para almacenar un valor en la sesión del usuario.

Si bien el objeto request comparte similitudes con un diccionario, es importante tener en cuenta que no es un diccionario puro de Python. Es un objeto especializado de Django que proporciona funcionalidades específicas para manejar solicitudes HTTP en el contexto de un proyecto Django.

Dicho lo anterior, continuamos.

Agregar nuevos productos al carro.

Creemos un método para añadir productos al carrito o actualizar su cantidad. Añade los siguientes métodos agregar() y guardar_carro() a la clase 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

El método 'agregar()' toma los siguientes parámetros como entrada:

- `producto`: La instancia del producto que se va a agregar o actualizar en el carrito.
- `cantidad`: Un entero opcional que representa la cantidad del producto. Por defecto, es 1.
- `override_quantity`: Un booleano que indica si se debe sobrescribir la cantidad con la cantidad proporcionada (True), o si la nueva cantidad debe sumarse a la cantidad existente (False).

Se utiliza el ID del producto como clave en el diccionario de contenido del carrito. Se convierte el ID del producto a una cadena porque Django utiliza JSON para serializar datos de sesión, y JSON solo permite nombres de claves como cadenas.

El ID del producto es la clave, y el valor que se persiste es un diccionario con las cantidades y precios del producto. El precio del producto se convierte de decimal a cadena para serializarlo. Finalmente, se llama al método 'guardar_carro()' para guardar el carrito en la sesión.

El método 'guardar_carro()' marca la sesión como modificada mediante `session.modified = True`. Esto le indica a Django que la sesión ha cambiado y necesita ser guardada.


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

El método eliminar() elimina un producto dado del diccionario del carrito y llama al método guardar_carro() para actualizar el carrito en la sesión.

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

Este código se encarga de restar la cantidad de un producto en el carrito de compras.

- El método `restar_producto` toma un parámetro `producto`, que se espera que sea un objeto de la clase `Producto`.

- Si el id del producto se encuentra en el diccionario "carro" entonces se busca la clave del producto con producto_id y se busca dentro de ese diccionario la clave cantidad y se le resta una unidad. Se verifica que si la cantidad es menor que uno entonces se elimine el producto del carro.

- Finalmente, se llama al método `guardar_carro` para guardar los cambios en el carrito actualizado.

En resumen, este código disminuye la cantidad de un producto en el carrito de compras. Busca el producto en el diccionario `self.carro`, resta 1 a su cantidad y, si la cantidad resultante es menor que 1, elimina el producto del carrito. Luego, se guarda el carrito actualizado utilizando el método `guardar_carro`.


Limpiar el carro.


No se elimina el carro de la sesión pero si que se eliminan todas sus claves dejando el carro vacío.

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


Tendremos que iterar varias veces a través de los elementos contenidos en el carrito y acceder a las instancias de Producto relacionadas.

Para hacerlo, definiremos un método iter() en la clase.

En el método iter(), recuperas las instancias de Producto que están presentes en el carrito para incluirlas en los elementos del carrito. Copias el carrito actual en la variable 'carro' y añades las instancias de Producto a este.

filter(id__in=product_ids) es un método de consulta que filtra los productos basándose en la condición especificada. En este caso, estás filtrando los productos por su ID, utilizando el operador __in, que verifica si el ID del producto está dentro de la lista product_ids. Finalmente, iteras sobre los elementos del carrito, convirtiendo el precio de cada elemento de nuevo a decimal y añadiendo un atributo 'precio_total' a cada elemento. Este método iter() te permitirá iterar fácilmente sobre los elementos en el carrito en vistas y plantillas.

También necesitas una manera de devolver el número total de elementos en el carrito. Cuando se ejecuta la función len() en un objeto, Python llama a su método len() para recuperar su longitud. Hemos definido un método len() personalizado para devolver el número total de elementos almacenados en el carrito fácilmente.

También hemos añadido el método "get_total_price()" que devuelve el coste total del carrito que resulta de multiplicar el precio de los productos del carrito por sus cantidades.

Y finalmente agregamos el método clear() que lo que hace es eliminar el carrito de la sesión.


Colocación del carrito a la derecha en la página de la tienda.


Quiero colocar el carrito en la parte superior derecha, al lado de las fichas. La idea es mostrar un carrito de la compra en el que aparezca la cantidad de productos y el coste total. Posteriormente si pinchas en él, te llevará a una página donde se especifique en detalle el contenido del carrito. Algo como esto:


carro



Para ello justo antes de colocar las pestañas con los juegos, colocamos el código del carrito.

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 -->
...

- En la primera línea, se crea un contenedor `div` con el atributo `style="float:right"`. Esto se utiliza para colocar el carrito en un formato flotante en la parte derecha de la página web. El uso de `float:right` permite que el carrito aparezca alineado a la derecha, dejando el contenido restante a la izquierda.

- En la tercera línea, se utiliza la etiqueta `{% include "carro/widget.html" %}`. Esta etiqueta de Django se utiliza para incluir y renderizar el contenido de un archivo de plantilla llamado "widget.html" que se encontrará, porque tenemos que crearla todavía, posteriormente en la carpeta "carro" dentro de los templates de la tienda. 

Crea el archivo widget.html dentro de las plantillas de la tienda y para que esté más organizado dentro de un subdirectorio llamado "carro". Su código es el siguiente:

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 %}

Esta es una de las formas para colocar iconos con Bootstrap. Aquí colocamos el icono de un carrito y a su derecha un circulo rojo con la cantidad de elementos añadidos al carrito. Esto lo conseguimos estableciendo en la variable total_items la longitud de los elementos que están en el carro. Acuérdate que habíamos establecido un __len__ personalizado en la clase Carro, precisamente para esto. En la parte inferior aparecerá el coste total de los productos del carro {{carro.get_total_price}}

Ahora vamos con las vistas. 


Para ello vamos al archivo views.py de la aplicación "Carro". Para poder trabajar con el carro de la compra necesitamos importar su clase, la clase Carro con todos sus métodos y propiedades y también tenemos que importar los productos. 

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.


Si queremos añadir cosas al carro, lógicamente cada tarjeta de producto tiene que tener un botón que realice esta opción. Nos vamos al templates de la tienda y añadimos en la sección de cada consola el código necesario. Voy a mostrar el código para la PS4 pero habría que hacer lo mismo para cada una de las demás. 

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.

Crea el archivo urls.py dentro de la aplicación del Carro y añade el siguiente código.

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

Este código configura el enrutamiento de URLs en Django. 

1. `from django.urls import path`: Esto importa la función `path` del módulo `django.urls`, que se utiliza para definir las rutas URL en Django.

2. `from . import views`: Esto importa las vistas definidas en el archivo `views.py` del mismo directorio.

3. `app_name = 'carro'`: Esto establece el nombre de la aplicación, que se utiliza para evitar conflictos en la resolución de nombres de URL si tienes múltiples aplicaciones en tu proyecto Django.

4. `urlpatterns`: Es una lista de patrones de URL que se deben asociar a las vistas correspondientes.

   - `path('agregar/<int:producto_id>/', views.agregar_producto, name='agregar')`: Este patrón de URL se asocia a la vista `agregar_producto` cuando se accede a la URL `/agregar/<producto_id>/`. El `<int:producto_id>` define una parte variable de la URL que debe ser un entero y se pasa como argumento a la vista.

   - `path('eliminar/<int:producto_id>/', views.eliminar_producto, name='eliminar')`: Este patrón de URL se asocia a la vista `eliminar_producto` cuando se accede a la URL `/eliminar/<producto_id>/`. Al igual que en el caso anterior, `<int:producto_id>` define una parte variable de la URL que debe ser un entero.

   - `path('restar/<int:producto_id>/', views.restar_producto, name='restar')`: Este patrón de URL se asocia a la vista `restar_producto` cuando se accede a la URL `/restar/<producto_id>/`. Igual que antes, `<int:producto_id>` define una parte variable de la URL que debe ser un entero.

   - `path('limpiar/', views.limpiar_carro, name='limpiar')`: Este patrón de URL se asocia a la vista `limpiar_carro` cuando se accede a la URL `/limpiar/`. No hay partes variables en esta URL.

     - `path('mostrar/', views.mostrar_carro, name='mostrar')`: Este patrón de URL se asocia a la vista `limpiar_carro` cuando se accede a la URL `/limpiar/`. No hay partes variables en esta URL.

Cada patrón de URL se asocia a una vista específica definida en el archivo `views.py`, y el parámetro `name` se utiliza para referirse a las URLs de manera fácil y legible en otras partes del código.

Y como siempre tenemos que entrar en urls.py de la aplicación PracticaDjango y añadir las urls de la aplicación carro:

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.


Un context processor es una función en Python que toma el objeto de solicitud como argumento y devuelve un diccionario que se agrega al contexto de la solicitud. Los context processors son útiles cuando necesitas hacer algo disponible globalmente para todas las plantillas.

Por defecto, cuando creas un nuevo proyecto usando el comando startproject, tu proyecto contiene los siguientes context processors de plantilla en la opción context_processors dentro de la configuración TEMPLATES:
- django.template.context_processors.debug: Esto establece las variables Boolean debug y sql_queries en el contexto, representando la lista de consultas SQL ejecutadas en la solicitud.
- django.template.context_processors.request: Esto establece la variable request en el contexto.
- django.contrib.auth.context_processors.auth: Esto establece la variable user en la solicitud.
- django.contrib.messages.context_processors.messages: Esto establece una variable messages en el contexto que contiene todos los mensajes generados usando el framework de mensajes.

Django también habilita django.template.context_processors.csrf para evitar ataques de falsificación de solicitudes entre sitios (CSRF). Este context processor no está presente en la configuración, pero siempre está habilitado y no se puede desactivar por razones de seguridad.

Puedes ver la lista de todos los context processors integrados en https://docs.djangoproject.com/en/5.0/ref/templates/api/#built-in-template-context-processors.

En resumen, es una variable que esta disponible en todo el proyecto a nivel global de forma que este disponible o se ejecute cada vez que se cargue cualquier vista. Nos va a servir para que el carro muestre el número de productos del carrito y su importe total. 


Configurando el carrito en el contexto de la solicitud


Creemos un context processor para establecer el carrito actual en el contexto de la solicitud. Con esto, podrás acceder al carrito en cualquier plantilla.

Crea un archivo nuevo dentro del directorio de la aplicación del Carro y llámalo context_processors.py. Los context processors pueden estar en cualquier lugar de tu código, pero al colocarlos aquí mantendrás tu código bien organizado.

Añade el siguiente código al archivo:


PracticaDjango/Carro/context_processor.py

from .carro import Carro

def carro(request):
    return {'carro': Carro(request)}

En tu context processor, instancias el carrito utilizando el objeto de solicitud y lo pones a disposición de las plantillas como una variable llamada carro.


Para que esto sea accesible desde cualquier parte del proyecto tenemos que registrarlo en el archivo settings.py del proyecto, dentro del apartado "TEMPLATES".


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.

Los context processors se ejecutan en todas las solicitudes que utilizan RequestContext. Si tu funcionalidad no es necesaria en todas las plantillas, especialmente si implica consultas a la base de datos, puede que quieras crear una etiqueta de plantilla personalizada en lugar de un context processor.

Bueno, una vez que hemos creado el icono del carro y hemos visto como agregar los productos, nos vamos a poner con como mostrar el contenido del carrito para poder eliminar los productos o restar un producto de la lista, así como un botón para volver a comprar más productos o finalizar la misma.

Y ya solamente nos queda construir la plantilla html que mostrará el detalle de los productos del carro.

Creamos un archivo llamado carro_detalle.html.

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 %}
   

Y ya está. Si el carro no contiene ningún producto, se mostrara un mensaje de advertencia como este.

cesta vacía


y si tiene algún producto la imagen será parecida a esta.


imagen de la cesta de la compra



Registrar los pedidos de los clientes.


Cuando se finaliza una compra en el carrito de compras, es necesario guardar un pedido en la base de datos. Los pedidos contendrán información sobre los clientes y los productos que están comprando.

Crea una nueva aplicación para gestionar pedidos de clientes usando el siguiente comando:

python manage.py startapp Orders

Edita el archvio settings.py del proyecto y registra la aplicación de la siguiente manera:

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

Necesitarás un modelo para almacenar los detalles del pedido y un segundo modelo para almacenar los artículos comprados, incluyendo su precio y cantidad. Edita el archivo models.py de la aplicación de pedidos y agrega el siguiente código a él:

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

El modelo Order contiene varios campos para almacenar información del cliente y un campo Booleano llamado 'paid', que por defecto se establece en False. Más adelante, usarás este campo para diferenciar entre pedidos pagados y no pagados. También hemos definido un método get_total_cost() para obtener el costo total de los artículos comprados en este pedido.

El modelo OrderItem te permite almacenar el producto, la cantidad y el precio pagado por cada artículo. Hemos definido un método get_cost() que devuelve el costo del artículo multiplicando el precio del artículo por la cantidad.

Para crear las migraciones iniciales para la aplicación de pedidos, ejecuta el siguiente comando:

python manage.py makemigrations


Verás una salida similar a la siguiente:

```
Migraciones para 'orders':
orders/migrations/0001_initial.py
- Crear modelo Order
- Crear modelo OrderItem
- Crear índice orders_orde_created_743fca_idx en el campo(s) -created del modelo order
```

Luego, ejecuta el siguiente comando para aplicar la nueva migración:

python manage.py migrate


Esto aplicará las migraciones recién creadas a tu base de datos.


Incluyendo los modelos en el panel de administración.


Para incluir los modelos de pedido en el sitio de administración, edita el archivo admin.py de la aplicación de pedidos y agregar el siguiente código:

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]

Se está utilizando una clase ModelInline para el modelo OrderItem para incluirlo como un "inline" en la clase OrderAdmin. Un inline te permite incluir un modelo en la misma página de edición que su modelo relacionado.

El comando `python manage.py runserver` se utiliza para ejecutar el servidor de desarrollo. Al acceder a http://127.0.0.1:8000/admin/Orders/order/add/en tu navegador, verás la página correspondiente.

The Add order form, including OrderItemInline



Creación de órdenes de clientes


Utilizarás los modelos de pedido que creaste para guardar los artículos contenidos en el carrito de compras cuando el usuario finalmente realiza un pedido. Se creará un nuevo pedido siguiendo estos pasos:

1. Presentar al usuario un formulario de pedido para que complete sus datos

2. Crear una nueva instancia de Pedido con los datos ingresados y crear una instancia asociada de OrderItem para cada ítem en el carrito

3. Borrar todo el contenido del carrito y redirigir al usuario a una página de finalización del pedido correcta.

Primero, necesitas un formulario para ingresar los detalles del pedido. Crea un nuevo archivo dentro del directorio de la aplicación de pedidos y nómbralo forms.py. Agrega el siguiente código:

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"]
Este es el formulario que vas a utilizar para crear nuevos objetos Order. Ahora necesitas una vista para manejar el formulario y crear un nuevo pedido. Edita el archivo views.py de la aplicación de pedidos y agrega el siguiente código resaltado:

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})
En la vista order_create, obtienes el carrito actual de la sesión con carro = Carro(request).

Dependiendo del método de solicitud, realizas las siguientes tareas:

- Solicitud GET: Instancias el formulario OrderCreateForm y renderizas la plantilla orders/order/create.html.

- Solicitud POST: Validas los datos enviados en la solicitud. Si los datos son válidos, creas un nuevo pedido en la base de datos usando order = form.save(). Iteras sobre los elementos del carrito y creas un OrderItem para cada uno de ellos. Finalmente, borras el contenido del carrito y renderizas la plantilla orders/order/created.html.

Crea un archivo nuevo dentro del directorio de la aplicación de pedidos y llámalo urls.py. Agrega el siguiente código:

PracticaDjango/Orders/urls.py

from django.urls import path
from . import views

app_name = 'orders'

urlpatterns = [
path('create/', views.order_create, name='order_create'),
]
Este es el patrón de URL para la vista order_create.

Edita el archivo urls.py de la aplicación principal e incluye el siguiente patrón. La nueva línea está resaltada en negrita:

PracticaDjango/PracticaDjango/urls.py

urlpatterns = [
    #...
    path('orders/', include('Orders.urls', namespace='orders')),
]
Edita la plantilla Tienda/carro_detalle.html de la aplicación Tienda y encuentra esta línea:

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>
y cambia el # de la línea resaltada por este código:

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>

Los usuarios ahora pueden navegar desde la página de detalle del carrito hasta el formulario de pedido.

Aún necesitas definir plantillas para crear pedidos. Crea la siguiente estructura de archivos dentro del directorio de la aplicación de órdenes (Orders):

templates/
        Orders/
                order/
                        create.html
                        created.html

Edita la plantilla Orders/order/create.html y agrega el siguiente código:

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 %}
Esta plantilla muestra los elementos del carrito, incluyendo totales y el formulario para realizar un pedido.

Edita la plantilla Orders/order/created.html y agrega el siguiente código:

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 %}
Esta es la plantilla que se muestra cuando se crea el pedido correctamente.

Inicia el servidor de desarrollo web para cargar los nuevos archivos. Abre http://127.0.0.1:8000/ en tu navegador, ve a la tienda, agrega un par de productos al carrito y continúa hacia la página de pago. 

Verás el siguiente formulario:

formulario para completar el pedido



Completa el formulario con datos válidos y haz clic en el botón 'Realizar pedido'. Se creará el pedido y verás una página de éxito como esta:

pedido realizado con éxito




Puedes encontrar el contenido de este capítulo en este enlace de github.


No hay comentarios:

Publicar un comentario