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.


viernes, 16 de junio de 2023

18.- Creación de la aplicación Tienda (menú de pestañas, tarjetas con Bootstrap5 y easy-thumbnails)

En este capítulo desarrollaremos parte de la aplicación Tienda. Aquí mostraremos los diferentes productos que pretendemos vender en ella. A través de un menú de pestañas mostraremos los diferentes juegos que tenemos para las diferentes plataformas. Luego mostraremos los juegos de cada plataforma a través de tarjetas ("cards") de bootstrap5, cuatro por cada fila. Quedará algo similar a esto:


ejemplo de tienda web

La creación de la aplicación es muy similar al resto que hemos visto hasta ahora:

1.- Creamos la nueva aplicación que gestionará la tienda.

$ python manage.py startapp Tienda

2.- Una vez creada la registramos:

PracticaDjango/PracticaDjango/settings.py

...
INSTALLED_APPS = [
    # Nuestras aplicaciones
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    'Servicios.apps.ServiciosConfig',
    'Blog.apps.BlogConfig',
    'Contacto.apps.ContactoConfig',
    'Tienda.apps.TiendaConfig',
    # Aplicaciones de terceros
    'django_bootstrap5',
    # Mapa del Sitio
    'django.contrib.sites', # add sites to installed_apps
    'django.contrib.sitemaps',  # add Django sitemaps to installed app
    # PostgreSQL
    'django.contrib.postgres',
    # Aplicaciones por defecto
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
...
Ahora lo que necesitamos es que cuando se vaya a la URL /tienda/, Django debe buscar la ruta en el archivo urls.py pero de la aplicación "Tienda", no en la del proyecto. Vamos a hacer las modificaciones necesarias.

Primeramente en el archivo:

PracticaDjango/PracticaDjango/urls.py: 

from django.contrib import admin
from django.urls import path, include
# Para registrar los archivos de las imagenes y poder verlas
from django.conf import settings
from django.conf.urls.static import static

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')),
]
urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
y luego en archivo que gestiona las urls de la tienda. Entra en el directorio de la aplicación Tienda, crea el archivo urls.py y añade el siguiente código:

PracticaDjango/Tienda/urls.py: 

from django.urls import path
# load views of these applications.
from . import views

app_name = 'Tienda'

urlpatterns = [
    path('', views.tienda, name='tienda'),
]

Más tarde crearemos la vista y la plantilla a renderizar. Pero antes vamos a preparar otras cosas.


Creación de modelos para el catálogo de productos.


3.- Como vamos a vender juegos de consola necesitamos definir dos cosas. Una categoría para registrar las diferentes consolas para las que vendemos los juegos y luego otra categoría para los propios juegos en si. Es decir, la categoría de las consolas será (ps4, ps5, xbox y nintendo)  y luego las categorías de los juegos tendrán sus propios campos como nombre del juego, categoría, que es la consola a la que pertenecen, una pequeña descripción, precio, si están disponibles en el stock de la tienda y una imagen. Los productos tendrán también un campo en el que se registrará la fecha en la que fue creada y la fecha en la que se actualiza. Para ello creamos el archivo models.py dentro de la aplicación Tienda y codificamos lo anterior de la siguiente forma:

PracticaDjango/Tienda/models.py: 

from django.db import models

# Create your models here.

# Creamos dos modelos para la categoría del producto (tipo de consola)
# y para el producto (el juego en si).

class CategoriaProducto(models.Model):
    '''Registrará las diferentes consolas para las que vendemos juegos'''
    nombre = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)

    class Meta:
        ordering = ["nombre"]
        indexes = [
            models.Index(fields=["nombre"]),
        ]
        verbose_name = "categoriaProducto"
        verbose_name_plural = "categoriasProductos"

    def __str__(self):
        return self.nombre
    

class Producto(models.Model):
    """Registra los propios juegos en si."""

    nombre = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200)
    categoria = models.ForeignKey(
        CategoriaProducto, on_delete=models.CASCADE, related_name="categoria_productos"
    )
    descripcion = models.CharField(blank=True)
    precio = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.BooleanField(default=True)
    # hay que tener instalado la libreria pillow para poder subir imagenes
    imagen = models.ImageField(upload_to="Tienda", blank=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["nombre"]
        indexes = [
            models.Index(fields=["id", "slug"]),
            models.Index(fields=["nombre"]),
            models.Index(fields=["-created"]),
        ]
        verbose_name = "producto"
        verbose_name_plural = "productos"

    def __str__(self):
        return self.nombre

Importante: Para trabajar con las imágenes de los juegos es absolutamente imprescindible que tengas instalada la librería Pillow.(pip install Pillow)

El catálogo de nuestra tienda consistirá en una serie de juegos que estarán organizados en diferentes categorías que serán los diferentes tipos de consolas. Cada juego tendrá su nombre, una descripción opcional, una imagen opcional, un precio y si existe disponibilidad o stock del mismo.

En el código superior hemos creado los modelos CategoríaProducto y Producto. El modelo de CategoríaProducto consta de un campo de nombre y un campo único de "slug" (único implica la creación de un índice). En la clase Meta del modelo de CategoríaProducto, hemos definido un índice para el campo de nombre.

Los campos del modelo de Producto son los siguientes:

• categoria: Una clave foránea (ForeignKey) al modelo de Categoría. Esta es una relación de uno a muchos: un producto pertenece a una categoría y una categoría contiene múltiples productos.
• nombre: El nombre del producto.
• slug: El "slug" para este producto para construir URLs bonitas.
• imagen: Una imagen opcional del producto.
• descripcion: Una descripción opcional del producto.
• precio: Este campo utiliza el tipo decimal.Decimal de Python para almacenar un número decimal de precisión fija. El número máximo de dígitos (incluyendo los lugares decimales) se establece utilizando el atributo max_digits y los lugares decimales con el atributo decimal_places.
• stock: Un valor booleano que indica si el producto está disponible o no. Se utilizará para habilitar/deshabilitar el producto en el catálogo.
• created: Este campo almacena cuándo se creó el objeto.
• updated: Este campo almacena cuándo se actualizó el objeto.

Para el campo de precio, usamos DecimalField en lugar de FloatField para evitar problemas de redondeo.

En la clase Meta del modelo de Producto, hemos definido un índice de múltiples campos para los campos id y slug. Ambos campos están indexados juntos para mejorar el rendimiento de las consultas que utilizan los dos campos.

Siempre utiliza DecimalField para almacenar cantidades monetarias. FloatField utiliza internamente el tipo float de Python, mientras que DecimalField utiliza el tipo Decimal de Python. Al usar el tipo Decimal, evitarás problemas de redondeo de los números flotantes.

Planeamos consultar productos en principio por su id aunque dejamos abierta la puerta para hacerlo con su "slug". Hemos añadido un índice para el campo de nombre y otro para el campo de creación. Hemos utilizado un guión antes del nombre del campo para definir el índice con un orden descendente.

Models for the product catalog



Registrando los modelos en el panel de administración.



4.- Puesto que tenemos que meter los tipos de consolas y los juegos de las diferentes plataformas, lo vamos a hacer a través del panel de Administración de Django, por lo cual creamos el correspondiente archivo admin.py dentro de la aplicación Tienda. Importamos los modelos que hemos creado y luego añadimos el siguiente código.

PracticaDjango/Tienda/admin.py: 

from django.contrib import admin

from .models import *

# Register your models here.

class CategoriaProductoAdmin(admin.ModelAdmin):
    list_display = ["nombre", "slug"]
    prepopulated_fields = {"slug": ("nombre",)}


class ProductoAdmin(admin.ModelAdmin):
    list_display = ["nombre", "slug", "precio", "stock", "created", "updated"]
    list_filter = ["stock", "created", "updated"]
    list_editable = ["precio", "stock"]
    prepopulated_fields = {"slug": ("nombre",)}

# Registramos ambas tablas y clases
admin.site.register(CategoriaProducto, CategoriaProductoAdmin)
admin.site.register(Producto, ProductoAdmin) 

Recuerda que se utiliza el atributo prepopulated_fields para especificar campos donde el valor se establece automáticamente usando el valor de otros campos. Como has visto anteriormente, esto es conveniente para generar "slugs".

Se usa el atributo list_editable en la clase ProductoAdmin para establecer los campos que se pueden editar desde la página de visualización de la lista del sitio de administración. Esto te permitirá editar múltiples filas a la vez.

Cualquier campo en list_editable también debe estar incluido en el atributo list_display, ya que solo los campos mostrados pueden ser editados.


Construyendo las vistas.


5.- Necesitamos crear el archivo de vistas que renderizará la plantilla y  los datos de la tienda. Para ello creamos el archivo views.py:

PracticaDjango/Tienda/views.py: 

from django.shortcuts import render

# Como trabajamos con productos vamos a importarlos
from Tienda.models import Producto

# Create your views here.

def tienda(request):
    productos = Producto.objects.filter(stock=True)
    # carga en la variable productos todos los juegos que hayamos introducido a través
    # del panel de administración de Django.
    contexto = {
        "productos": productos
    }
    
    return render(request, "Tienda/tienda.html", contexto)   
   


En el código anterior, hemos realizado la búsqueda con el filtro stock=True para devolver solamente los productos que estén disponibles.




Puesto que anteriormente hemos creado el archivo models.py y realizado modificaciones, antes de seguir adelante tenemos que realizar las migraciones. Para ello ejecuta en el shell de python las siguientes instrucciones:

python manage.py makemigrations

python manage.py migrate


Para luego probar que todo funciona aquí deberías parar un momento y entrar en el panel de administración de Django y poner algunos datos de consolas y juegos. Si quieres usar la plantilla html que voy a explicar luego tienes que crear cuatro tipos de consolas (PS4, PS5, XBOX y NINTENDO) dentro del apartado CategoriaProductos y luego al menos en una de las categorías poner cuatro juegos. (Dentro del campo Productos)


6.- Ahora viene lo más importante que es crear la plantilla tienda.html. No te asustes porque aunque es un poco larga, es muy sencilla de explicar e iremos paso a paso. Lo primero es crear el directorio templates/Tienda dentro de la aplicación Tienda. Luego creamos el archivo tienda.html. Pero antes de teclear código, tenemos que borrar todo lo que creamos al principio en la aplicación Proyecto_web_app y que hacia referencia a la tienda cuando teníamos la web en pruebas. Borraremos en urls.py la línea que hace referencia a el path de la tienda, en views.py la vista de la tienda, y en el template la plantilla tienda.html. Todo esto está dentro de Proyecto_web_app. Ahora introducimos el código html de la página de la tienda, ya en su aplicación correspondiente.


PracticaDjango/Tienda/templates/Tienda/tienda.html 

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}

<!-- Establecemos el titulo de la página -->
{% block title %}Tienda{% endblock %}

<!-- Definimos su contenido -->
{% block content %}
<h1 class="text-center">Elige tu Consola.</h1>

<div class="container">
  <div class="bg-dark">
    <ul class="nav nav-tabs">
      <li class="nav-item">
        <a class="nav-link active" id="ps4-tab" data-bs-toggle="tab" href="#ps4" role="tab" aria-controls="ps4"
          aria-selected="true">PS4</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" id="ps5-tab" data-bs-toggle="tab" href="#ps5" role="tab" aria-controls="ps5"
          aria-selected="false">PS5</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" id="xbox-tab" data-bs-toggle="tab" href="#xbox" role="tab" aria-controls="xbox"
          aria-selected="false">Xbox</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" id="xbox-tab" data-bs-toggle="tab" href="#nintendo" role="tab" aria-controls="xbox"
          aria-selected="false">Nintendo</a>
      </li>
    </ul>
  </div>

  <div class="tab-content">
    <div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
      <h3>Juegos PS4</h3>
      <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">{{producto.nombre}}</h4>
              <p class="card-text">{{producto.precio}} €</p>
              <a href="#" class="btn btn-primary">See Profile</a>
            </div>
          </div>
        </div>
        {% endif %}
        {% endfor %}
      </div>
    </div>

    <div class="tab-pane fade" id="ps5" role="tabpanel" aria-labelledby="ps5-tab">
      <h3>Juegos PS5</h3>
      <!-- Contenido para juegos PS5 -->
      <div class="row g-4">
        {% for producto in productos %}
          {% if producto.categoria_id == 2 %}
            <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">{{producto.nombre}}</h4>
                  <p class="card-text">{{producto.precio}} €</p>
                  <a href="#" class="btn btn-primary">See Profile</a>
                </div>
              </div>
            </div>
          {% endif %}
        {% endfor %}
      </div>
    </div>
    <div class="tab-pane fade" id="xbox" role="tabpanel" aria-labelledby="xbox-tab">
      <h3>Juegos Xbox</h3>
      <!-- Contenido para juegos Xbox -->
      <div class="row g-4">
        {% for producto in productos %}
        {% if producto.categoria_id == 3 %}
        <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">{{producto.nombre}}</h4>
              <p class="card-text">{{producto.precio}} €</p>
              <a href="#" class="btn btn-primary">See Profile</a>
            </div>
          </div>
        </div>
        {% endif %}
        {% endfor %}
      </div>
    </div>
    <div class="tab-pane fade" id="nintendo" role="tabpanel" aria-labelledby="xbox-tab">
      <h3>Juegos Nintendo</h3>
      <!-- Contenido para juegos Nintendo -->
      <div class="row g-4">
        {% for producto in productos %}
        {% if producto.categoria_id == 4 %}
        <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">{{producto.nombre}}</h4>
              <p class="card-text">{{producto.precio}} €</p>
              <a href="#" class="btn btn-primary">See Profile</a>
            </div>
          </div>
        </div>
        {% endif %}
        {% endfor %}
      </div>
    </div>
  </div>
</div>
{% endblock %}

Esta plantilla de Django es utilizada para renderizar una página web que muestra diferentes juegos de consolas divididos en pestañas. A continuación, explicaré cada parte de la plantilla en detalle:

  1. Carga de la plantilla base:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

{% extends "Proyecto_web_app/base.html" %}
Esta línea indica que esta plantilla hereda de otra plantilla base llamada "base.html". La plantilla base es utilizada para establecer la estructura común de todas las páginas en el proyecto.


2. Establecimiento del título de la página:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

{% block title %}Tienda{% endblock %}
Aquí se define el título de la página, que se mostrará en la pestaña del navegador. En este caso, el título se establece como "Tienda".

3. Definición del contenido:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

{% block content %}
...
{% endblock %}
Todo el contenido de la página se encuentra dentro de este bloque. Permite que la plantilla base reemplace este bloque con contenido específico de cada página.

4. Encabezado:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<h1 class="text-center">Elige tu Consola.</h1>
Este es un encabezado de nivel 1 que se muestra en la página. Muestra el texto "Elige tu Consola." y se alinea al centro.

5. División en pestañas:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<div class="container">
  <div class="bg-dark">
    <ul class="nav nav-tabs">
      ...
    </ul>
  </div>

En esta sección se crea un conjunto de pestañas utilizando el componente de navegación de Bootstrap. Cada pestaña representa una categoría de juegos de consolas.

6. Contenido de las pestañas:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<div class="tab-content">
  <div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
    ...
  </div>
  <div class="tab-pane fade" id="ps5" role="tabpanel" aria-labelledby="ps5-tab">
    ...
  </div>
  <div class="tab-pane fade" id="xbox" role="tabpanel" aria-labelledby="xbox-tab">
    ...
  </div>
  <div class="tab-pane fade" id="nintendo" role="tabpanel" aria-labelledby="xbox-tab">
    ...
  </div>
</div>
Aquí se define el contenido de cada pestaña. Cada bloque <div class="tab-pane fade"> representa el contenido de una pestaña específica. El atributo id identifica el contenido de cada pestaña, y el atributo role especifica el papel de la pestaña.

7. Bucle de juegos por categoría:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

{% for producto in productos %}
  {% if producto.categoria_id == 1 %}
  ...
  {% endif %}
{% endfor %}
La variable "productos" se la hemos pasado a la plantilla a través de la vista y recoge todos los juegos que tenemos en la tienda. Este objeto tiene todos los juegos, independientemente de su categoría, es por ello por lo que tenemos que iterar sobre ellos y usar un condicional para que solo muestre en cada pestaña que hemos creado los juegos que pertenecen a la misma.

En otras palabras este es un bucle for de Django que itera sobre una lista de productos. Dentro del bucle, se comprueba si el producto pertenece a una categoría específica (identificada por el atributo categoria_id). En este ejemplo, se muestra el código 1 ya que corresponde a la categoria_id de PS4. La 2 es la de PS5, la 3 es XBOX y la 4 a Nintendo.

8. Tarjetas de juego.

Una vez que ya tenemos que juegos pertenecen a cada pestaña vamos a mostrar los mismos usando las cards de Bootstrap. Para que quede bien, vamos a poner cuatro cartas en cada fila.

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<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">{{producto.nombre}}</h4>
      <p class="card-text">{{producto.precio}} €</p>
      <a href="#" class="btn btn-primary">See Profile</a>
    </div>
  </div>
</div>
Este bloque representa una tarjeta de juego individual que se muestra en la página. Muestra la imagen del juego (producto.imagen.url), el nombre del juego (producto.nombre), el precio (producto.precio) y un botón de "See Profile". Se utiliza la sintaxis de plantillas de Django ({{ ... }}) para incrustar los valores de los atributos de los productos dentro de la plantilla.

Entremos un poco más en detalles.

Todo este código esta englobado en una fila, ya que hemos utilizado:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<div class="row g-4">
  ...
</div>

La línea <div class="row g-4"> es una clase de Bootstrap 5 que se utiliza para crear una fila (row) en un sistema de grillas (grid). A continuación se explica su significado:

  • <div>: Es un elemento HTML de división utilizado para agrupar contenido.

  • class="row": Es una clase de Bootstrap que define una fila en el sistema de grillas. Las filas son utilizadas para organizar el contenido en columnas dentro de un contenedor.

  • g-4: Es una clase de Bootstrap que agrega un espacio (g) entre las columnas dentro de la fila. El número 4 indica el tamaño del espacio en píxeles. En este caso, se establece un espacio de 4 píxeles entre las columnas.
Bien, ahora que tenemos una fila queremos que se distribuyan cuatro cartas por cada fila. ¿Y como conseguimos esto? Pues muy sencillo, como cada fila o "row" de bootstrap consta de 12 columnas  y queremos poner 4 juegos en cada fila, le corresponderían 3 columnas para cada juego. ( 4 juegos * 3 columnas = 12 columnas totales)

Lo reflejamos con el siguiente código:

/PracticaDjango/Tienda/templates/Tienda/tienda.html

<div class="col-md-3">
...
</div>
El resumen de todo lo anterior es que esta plantilla en particular renderiza una página que muestra juegos de diferentes consolas en pestañas separadas. Cada pestaña contiene tarjetas de juegos correspondientes a la categoría de consola respectiva. La plantilla hace uso de la herencia de plantillas, carga de contenido estático, bucles y condicionales para generar dinámicamente el contenido de la página.

Una vez realizado lo anterior se vería algo así como esto:

ejemplo de tienda web


Puedes encontrar el código de este capítulo en este enlace en github.

Anexo.

Creación de miniaturas de imagen usando easy-thumbnails

Estamos mostrando las imágenes originales en la página de la tienda, pero las dimensiones de diferentes imágenes pueden variar considerablemente. El tamaño de archivo de algunas imágenes puede ser muy grande y cargarlas podría llevar demasiado tiempo.

La mejor manera de mostrar imágenes optimizadas de manera uniforme es generar miniaturas. Una miniatura es una representación pequeña de una imagen más grande. Las miniaturas cargarán más rápido en el navegador y son una excelente manera de homogeneizar imágenes de tamaños muy diferentes. Utilizaremos una aplicación de Django llamada easy-thumbnails para generar miniaturas de las imágenes de los diferentes juegos de la tienda.

Abre la terminal e instala easy-thumbnails usando el siguiente comando:

pip install easy-thumbnails

Edita el archivo settings.py del proyecto y agrega easy_thumbnails al ajuste INSTALLED_APPS, de la siguiente manera:

PracticaDjango/PracticaDjango/settings.py

INSTALLED_APPS = [
    #...
    # Aplicaciones de terceros
    #...
    'easy_thumbnails',

Luego, ejecuta el siguiente comando para sincronizar la aplicación con tu base de datos:

python manage.py migrate

La aplicación easy-thumbnails te ofrece diferentes formas de definir miniaturas de imágenes. La aplicación proporciona una etiqueta de plantilla {% thumbnail %} para generar miniaturas en plantillas y un campo de imagen personalizado (ImageField) si deseas definir miniaturas en tus modelos. Vamos a utilizar el enfoque de la etiqueta de plantilla para las imagenes de las tarjetas de la tienda.
Edita la plantilla Tienda/tienda.html de la aplicación Tienda y modifícala con el código resaltado en azul:

PracticaDjango/Tienda/templates/Tienda/tienda.html

<!--Cargamos la plantilla base-->
{% extends "Proyecto_web_app/base.html" %}
{% load thumbnail %}

<!-- ... -->

<div class="tab-pane fade show active" id="ps4" role="tabpanel" aria-labelledby="ps4-tab">
      <h3>Juegos PS4</h3>
      <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"> -->
            <img class="card-img-top" src="{% thumbnail producto.imagen 200x0 %}" alt="Card image">
            <div class="card-body">
              <h4 class="card-title">{{producto.nombre}}</h4>
              <p class="card-text">{{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>

Hemos definido una miniatura con un ancho fijo de 200 píxeles y una altura flexible para mantener la relación de aspecto utilizando el valor 0. La primera vez que un usuario carga esta página, se creará una imagen en miniatura. La miniatura se almacena en el mismo directorio que el archivo original. La ubicación está definida por el ajuste MEDIA_ROOT y el atributo upload_to del campo de imagen del modelo Producto. La miniatura generada luego será servida en las siguientes peticiones.
Ejecuta el servidor de desarrollo con el siguiente comando desde la terminal:

python manage.py runserver

Accede a la página de la tienda y compara la diferencia con la versión anterior.

pagina de tienda usando miniaturas


Si haces clic en una de las imágenes y la abres en una nueva pestaña


dirección url de la imagen


El nombre de archivo original va seguido de detalles adicionales de la configuración utilizada para crear la miniatura. Para una imagen PNG, verás un nombre de archivo como filename.png.200x0_q85.png, donde 200x0 son los parámetros de tamaño utilizados para generar la miniatura y 85 es el valor para la calidad predeterminada PNG utilizada por la biblioteca para generar la miniatura.

Puedes usar un valor de calidad diferente utilizando el parámetro quality. Para establecer la calidad PNG más alta, puedes usar el valor 100, así: {% thumbnail image.image 200x0 quality=100 %}. Una mayor calidad implicará un tamaño de archivo más grande.

La aplicación easy-thumbnails ofrece varias opciones para personalizar tus miniaturas, incluyendo algoritmos de recorte y diferentes efectos que se pueden aplicar. Si encuentras problemas al generar miniaturas, puedes agregar THUMBNAIL_DEBUG = True al archivo settings.py para obtener información de depuración.

Puedes leer la documentación completa de easy-thumbnails en https://easy-thumbnails.readthedocs.io/.