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




martes, 13 de junio de 2023

17.- Creación de la aplicación Contacto. (Formularios: Form y ModelForm)

Tenemos que crear una página con un formulario para que los usuarios nos envíen sus comentarios.

Django viene con dos clases que nos permitirán construir de forma sencilla formularios:

  • Form: nos permite construir formularios estandar definiendo los campos y las validaciones.
  • ModelForm: te permitirá construir formularios usando directamente el modelo. Proporciona todas las funcionalidades de la clase anterior, pero los campos del formulario se pueden declarar explícitamente o se pueden generar automáticamente desde los campos del modelo. El formulario se puede utilizar para crear o editar instancias del modelo.

Tendrá una apariencia similar a esto:

formulario final

Empezamos creando la APP. En consola utilizamos el comando:

$ python manage.py startapp Contacto

- Luego registramos la nueva aplicación. (aquí lo hacemos de otra forma diferente a como lo hemos hecho con las otras aplicaciones, usando Contacto.apps.ContactoConfig)

PracticaDjango/PracticaDjango/settings.py

...
INSTALLED_APPS = [
    # my applications
    'Proyecto_web_app',
    'Servicios',
    'Blog',
    'Contacto.apps.ContactoConfig',
    # third party applications
    'bootstrap5',
    # default aplications
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
...

- Posteriormente tenemos que decirle a Django que cuando se entre en la url /contacto se ejecute una determinada vista que será la encargada de renderizar el formulario de contacto. Para ello definiremos la ruta url de la aplicación, primeramente en el archivo urls.py del proyecto.

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('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('', include('Proyecto_web_app.urls')),
]
urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Es decir cuando se vaya a la URL /contacto/, Django debe buscar la ruta en el archivo urls.py pero de la aplicación "Contacto", no en la del proyecto.

Entra en el directorio de la aplicación Contacto y crea el archivo urls.py. Luego añade el siguiente código:

PracticaDjango/Contacto/urls.py: 

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

app_name = 'Contacto'

urlpatterns = [
    path('', views.contacto, name='contacto'),
]
1. `from django.urls import path`: Esta línea importa la función `path` del módulo `django.urls`. La función `path` se utiliza para definir las rutas URL de la aplicación.

2. `from . import views`: Esta línea importa las vistas (funciones) definidas en el archivo `views.py` del directorio actual (indicado por `.`). Las vistas son responsables de manejar las solicitudes HTTP y devolver una respuesta. Todavia no existe lo crearemos en breve.

3. `urlpatterns = [ ... ]`: Aquí comienza la lista de patrones de URL. La variable `urlpatterns` es una lista que contiene las rutas URL de la aplicación.

4. `path('', views.contacto, name='contacto')`: Esta línea define una ruta URL vacía (raíz) que se asigna a la vista `contacto` del archivo `views.py`. El primer argumento `''` representa la ruta URL (en este caso, la raíz del sitio web). El segundo argumento `views.contacto` especifica la vista que se debe llamar cuando se accede a esta URL. El tercer argumento `name='contacto'` es un nombre opcional para esta ruta URL, que se puede utilizar para referirse a ella en otras partes del código.

En resumen, este código define una única ruta URL vacía (`''`) que se asigna a la vista `contacto`. Cuando se accede a la raíz del sitio web, se llamará a la vista `contacto` para manejar la solicitud.

- Para manejar los templates de esta aplicación crearemos el directorio siguiente:

$ PracticaDjango/Contacto/templates/Contacto/

y dentro crearemos el archivo "contacto.html" que será el que contenga el código HTML del formulario de la aplicación que construiremos más adelante.

- Tenemos que editar el archivo de vistas "views.py" de la aplicación que será el que ejecute la función designada cuando se entre el la url de contacto.

PracticaDjango/Contacto/views.py: 

from django.shortcuts import render

def contacto(request):
   return render(request, "Contacto/contacto.html")
Y con esto tendremos la estructura básica de la aplicación.

***Vamos a crear el formulario usando ambas clases para ver como funcionan y en que situaciones usar cada una.

A) Creación del formulario con la clase Form.


- Ahora crearemos el formulario de contacto. Tenemos en Django la clase Forms, como ya comentamos al principio de la entrada, que nos va a ayudar a crear nuestros formularios de manera sencilla. Lo primero que tenemos que hacer es crear un archivo llamado forms.py, importar la librería forms y después crear una clase con el nombre que le queramos dar al formulario heredando de forms.Form y empezar a construir los campos de entrada de los datos que queramos usar.

Creamos el archivo forms.py, tal como dice el manual de Django:

PracticaDjango/Contacto/forms.py

from django import forms

class FormularioContacto(forms.Form):
    # Especificamos los campos del formulario
    nombre = forms.CharField(label="Nombre", max_length=50, required=True)
    email = forms.EmailField(label="Email", max_length=50, required=True)
    contenido = forms.CharField(label="Contenido", max_length=400, widget=forms.Textarea(attrs={'cols': 45, 'rows': 5}))

Nota: los formularios pueden estar en cualquier parte del código del proyecto. Sin embargo por convención se colocan dentro de cada aplicación en un archivo llamado forms.py.

De manera muy similar a cómo un modelo de Django describe la estructura lógica de un objeto, su comportamiento y la forma en que sus partes se nos presentan, una clase Form describe un formulario y determina cómo funciona y cómo aparece.

De forma similar a cómo los campos de una clase del archivo models.py se mapean a campos de base de datos, los campos de una clase de formulario se mapean a elementos <input> de formulario HTML. (Un ModelForm mapea los campos de una clase de modelo a elementos <input> de formulario HTML a través de un Form; esto es en lo que se basa el administrador de Django).

Los campos de un formulario son ellos mismos clases; administran los datos del formulario y realizan la validación cuando se envía el formulario. 

Un campo de formulario se representa para un usuario en el navegador como un "widget" HTML. Cada tipo de campo tiene una clase de widget predeterminada apropiada, pero estas pueden ser reemplazadas según sea necesario.

Tipos de campos en Django.

En el formulario de contacto queremos que se nos facilite un nombre, un email y un comentario.

Este código de Django define un formulario de contacto utilizando la biblioteca de formularios de Django. Aquí hay una explicación línea por línea del código:

1. `from django import forms`: Importa el módulo `forms` de la biblioteca Django, que contiene las clases para crear formularios.

3. `class FormularioContacto(forms.Form):` Define una clase llamada `FormularioContacto` que hereda de la clase `forms.Form`. Esto significa que `FormularioContacto` es un formulario de Django.

5. `nombre = forms.CharField(label="Nombre", max_length=50, required=True)`: Define un campo llamado `nombre` en el formulario. Este campo es de tipo `CharField`, que representa un campo de texto. El argumento `label` establece la etiqueta que se mostrará para este campo en el formulario. `max_length` especifica la longitud máxima del campo de texto, y `required` indica que este campo es obligatorio. Aunque no haría falta ponerlo porque por defecto si no se especifica otra cosa es un campo obligatorio, si quieres que sea opcional habría que poner required = False

6. `email = forms.EmailField(label="Email", max_length=50, required=True)`: Define un campo llamado `email` en el formulario. Este campo es de tipo `EmailField`, que valida automáticamente que el valor ingresado sea una dirección de correo electrónico válida.

7. `contenido = forms.CharField(label="Contenido", max_length=400, widget=forms.Textarea(attrs={'cols': 45, 'rows': 5}))`: Define un campo llamado `contenido` en el formulario. Este campo también es de tipo `CharField`, pero se usa un widget `Textarea` para permitir la entrada de texto multilínea. El argumento `attrs` se utiliza para especificar los atributos adicionales del widget, en este caso, se establece el número de columnas (`cols`) y filas (`rows`) del área de texto.


Manejo de formularios en las vistas.


Ahora tenemos que llevar esto que acabamos de crear a la vista. Necesitamos una vista para crear una instancia del formulario y manejar el envío del mismo. Así que abrimos el views.py de la aplicación 'Contacto' y lo primero que vamos a hacer es importar el formulario y crear una instancia del mismo. Luego se lo pasamos al render como parámetro un tercer elemento con un diccionario con la nomenclatura de nombre: valor.

PracticaDjango/Contacto/views.py: 

from django.shortcuts import render

# API del formulario
from .forms import FormularioContacto

def contacto(request):
    formulario_contacto = FormularioContacto()
    return render(request, "Contacto/contacto.html", {'form':formulario_contacto})

Vamos a ir creando un template básico para trabajar con el, aunque luego lo modifiquemos:

PracticaDjango/Contacto/templates/Contacto/contacto.html: 

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

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

<!-- Definimos su contenido -->
{% block content %}
<h1 class="text-center">Contacta con nosotros.</h1>

# Muestra el formulario que le hemos pasado.
<div><p>{{form}}</p></div>
{% endblock %}


Antes de nada tenemos que desvincular el enlace /contacto de la aplicación Proyecto_web_app que hasta ahora era la encargada de renderizarlo. Para ello tenemos que:

  1. ir al archivo Proyecto_web_app/views.py y borrar la vista 'contacto'. 
  2. ir al archivo Proyecto_web_app/urls.py y borrar el path correspondiente a la aplicación contacto.
  3. ir al directorio Proyecto_web_app/templates/ y borrar la plantila contacto.html.

Después de esto ya podemos ejecutar el servidor y ver que funciona. Abre el terminal y ejecuta:

(venv) $ python manage.py runserver

Desde el enlace CONTACTO o tecleando en el navegador http://127.0.0.1:8000/contacto/, comprueba que todo esta correcto.

Aunque los campos quedan muy feos, vemos que el formulario se renderiza correctamente (luego lo modificaremos para que quede más bonito y le añadiremos los botones):

formulario de contacto sin formato

Vamos a centrarnos en dar formato a este formulario. Si utilizamos Bootstrap para darle formato es muy sencillo. Ponemos el código y lo comento:

PracticaDjango/Contacto/templates/Contacto/contacto.html: 

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

<!--Cargamos de nuevo el contenido estático pra usar la etiqueta bootstrap_form-->
{% load django_bootstrap5 %}

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

<!-- Definimos su contenido -->
{% block content %}
<h1 class="text-center">Contacta con nosotros.</h1>

<!-- Muestra el formulario que le hemos pasado.-->
<form action="" method="POST">
    {% csrf_token %}
    <div class="container w-25 bg-primary rounded-1">
        {% bootstrap_form form %}
        <button type="submit" class="btn btn-primary">Enviar</button>
        <button type="reset" class="btn btn-secondary">Borrar</button>
    </div>
</form>
{% endblock %}

Este código es una plantilla HTML que muestra el formulario web. Aquí hay una explicación línea por línea:

1. `<form action="" method="POST">`: Esto crea la estructura del formulario HTML. El atributo `action` determina la URL a la que se enviarán los datos del formulario cuando se envíe, en este caso, se deja vacío porque se va a enviar la información a la misma url en la que ya estamos. El atributo `method` especifica el método HTTP utilizado para enviar los datos, en este caso, `POST`. 

Cuando acedemos a el formulario por primera vez, esta petición es de tipo GET.

Sin embargo para pasar la información del formulario vamos a utilizar el método POST. Cuando el usuario rellena el formulario y le da al botón enviar en realidad se está creando un diccionario con los datos introducidos que es la que se envía. Además cuando el usuario envíe la información debería salir un feed-back que comunicara el éxito o fracaso del envío. Esto lo codificaremos luego.

2. `{% csrf_token %}`: Esta es una etiqueta especial en el lenguaje de plantilla Django. Genera un campo de token de seguridad (CSRF) que protege el formulario contra ataques CSRF (Cross-Site Request Forgery). Django utiliza este token para verificar que las solicitudes POST provienen del mismo sitio web y no de una fuente maliciosa. Siempre tenemos que ponerla al crear el formulario.

3. `<div class="container w-25 bg-primary rounded-1">`: Esto crea un contenedor de estilo para el formulario. La clase `"container"` proporciona un diseño de contenedor en Bootstrap, mientras que las clases `"w-25"` definen el ancho del contenedor (25% del ancho del contenedor principal). Las clases `"bg-primary"` y `"rounded-1"` establecen el color de fondo azul y los bordes redondeados del contenedor respectivamente.

4. `{% bootstrap_form form %}`: Esta la etiqueta de Django que renderiza automáticamente los campos del formulario utilizando el paquete Django-Bootstrap. Renderiza el formulario `form` en el HTML generado. Es la responsable de crear el formulario tal como lo vemos. Puedes encontrar más información y opciones en https://django-bootstrap5.readthedocs.io/en/latest/templatetags.html

5. `<button type="submit" class="btn btn-primary">Enviar</button>`: Este es un botón de envío del formulario. Al hacer clic en él, se enviarán los datos del formulario. La clase `"btn btn-primary"` aplica estilos de Bootstrap al botón.

6. `<button type="reset" class="btn btn-secondary">Borrar</button>`: Este es un botón de reinicio del formulario. Al hacer clic en él, se restablecerán todos los campos del formulario a sus valores predeterminados. La clase `"btn btn-secondary"` aplica estilos de Bootstrap al botón.

En resumen, este código muestra un formulario web básico con campos generados automáticamente utilizando Django-Bootstrap. Los datos del formulario se enviarán mediante el método `POST` a una URL especificada en el atributo `action` del formulario. Además, se incluye un token de seguridad CSRF para proteger el formulario contra ataques maliciosos.

De esta forma tan simple nuestro formulario tendrá este bonito aspecto:

formulario con bootstrap

Si pruebas a intentar este formulario en blanco, tanto el propio navegador, como la libreria form de Django hacen una validación y te dicen que rellenes el campo que está en blanco, que cuando lo definimos dijimos que era obligatorio rellenarlo. También puedes probar a poner una dirección de correo electrónico que no sea válida y te dará también un error.

La mayoría de los navegadores modernos evitarán que envíes un formulario con campos vacíos o con errores. Esto se debe a que el navegador valida los campos en función de sus atributos antes de enviar el formulario. En este caso, el formulario no se enviará y el navegador mostrará un mensaje de error para los campos que estén incorrectos. Para probar la validación de formularios de Django utilizando un navegador moderno, puedes omitir la validación del formulario del navegador añadiendo el atributo "novalidate" al elemento HTML <form>, como por ejemplo <form method="post" novalidate>. Puedes agregar este atributo para evitar que el navegador valide los campos y probar tu propia validación de formularios. Después de que hayas terminado de probar, elimina el atributo "novalidate" para mantener la validación de formularios del navegador.

Puedes encontrar más información sobre cómo trabajar con formularios en https://docs.djangoproject.com/en/4.1/topics/forms/.

Ahora se trata de que la información que escriba el usuario se envíe. Cuando se rellenen los campos y se pulse en el botón "Enviar" la información se enviará a la URL /contacto/ usando el método Post. Esta información se envía a través de un diccionario. Para gestionarla tenemos que hacer lo siguiente en el archivo views.py:

PracticaDjango/Contacto/views.py: 

from django.shortcuts import render

# API del formulario
from .forms import FormularioContacto

def contacto(request):
    formulario_contacto = FormularioContacto()
    
    # Si se ha hecho "POST" rescata la información del diccionario enviado.
    if request.method=="POST":
        # El método post devuelve un diccionario con los datos del formulario
        formulario_contacto = FormularioContacto(data=request.POST)
        # Si el formulario de contacto es válido, se han rellenado los campos obligatorios
        # y los campos están bien definidos.
        if formulario_contacto.is_valid():
            datos = formulario_contacto.cleaned_data
            nombre = datos.get("nombre")
            email = datos.get("email")
            contenido = datos.get("contenido")
    return render(request, "Contacto/contacto.html", {'form':formulario_contacto})


Si la el método que se recibe es "POST", y se comprueba que el formulario es correcto, se almacenan los campos del diccionario en sus respectivas variables "nombre", "email" y "contenido". 

No obstante cuando el usuario pulse el botón enviar debería recibir un feedback, es decir un mensaje que le diga que la información se ha enviado correctamente. Para ello hay que tener en cuenta que cada vez que se pulsa el botón enviar, y se envía la información con el método POST, hay una recarga de página con la información introducida en el formulario. Lo que podemos hacer es a esa recarga enviarle un parámetro, una palabra por ejemplo. Y eso tendrá que estar dentro del "if". 

Para pasar un parámetro a contacto.html usaremos una redirección. 

PracticaDjango/Contacto/views.py: 

from django.shortcuts import render

# Para poder redireccionar a otras urls
from django.shortcuts import redirect

# API del formulario
from .forms import FormularioContacto

def contacto(request):
    formulario_contacto = FormularioContacto()
    
    # Si se ha hecho "POST" rescata la información del diccionario enviado.
    if request.method=="POST":
        # El método post devuelve un diccionario con los datos del formulario
        formulario_contacto = FormularioContacto(data=request.POST)
        # Si el formulario de contacto es válido, se han rellenado los campos obligatorios
        # y los campos están bien definidos.
        if formulario_contacto.is_valid():
            datos = formulario_contacto.cleaned_data
            nombre = datos.get("nombre")
            email = datos.get("email")
            contenido = datos.get("contenido")
            return redirect("/contacto/?valido")

    
    return render(request, "Contacto/contacto.html", {'form':formulario_contacto})


En Django, se utiliza la función `redirect()` para redirigir al usuario a una URL específica. En este caso, la URL es `"/contacto/?valido"`. 

El 'redirect()' es una función de utilidad que redirige al usuario a la URL especificada. Puede tomar como argumento una URL absoluta o una ruta relativa. En este caso, se proporciona una ruta relativa, que es "/contacto/?valido". El interrogante es como se pasa la información del parámetro cuando usamos el método GET. Después del ? podemos poner lo que queramos "valido", "ok" o algo similar.

"?valido" es el parámetro que se pasa a la URL. Generalmente los parámetros en una URL se utilizan para transmitir información adicional a la página de destino. En este caso, "?valido" indica que la página de contacto ha sido enviada exitosamente y se está utilizando como una señal para mostrar algún mensaje de éxito o realizar alguna otra acción en la página de destino. 

Para que se muestre un mensaje de feedback tenemos que modificar la plantilla:

PracticaDjango/Contacto/templates/Contacto/contacto.html: 

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

<!--Cargamos el contenido estático-->
{% load django_bootstrap5 %}

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

<!-- Definimos su contenido -->
{% block content %}
<h1 class="text-center">Contacta con nosotros.</h1>

<!-- Para que si el formulario tuviese errores nos avise -->
{% if form.errors %}
<div class="alert alert-warning">
    <strong>Warning!</strong> Por favor,revisa este campo..
</div>
{% endif %}

<!-- Si en una petición GET se envía el parámetro valido -->
{% if "valido" in request.GET %}
<div class="alert alert-success">
    <strong>Success!</strong> Información Enviada Correctamente, Muchas Gracias!.
</div>
{% endif %}

<!-- Si en una petición GET se envía el parámetro novalido -->
{% if "novalido" in request.GET %}
<div class="alert alert-danger">
    <strong>Danger!</strong> Ha habido un problema al enviar el correo, Vuelva a intentarlo!
</div>
{% endif %}

# Muestra el formulario que le hemos pasado.
<form action="" method="POST" class="form">
    {% csrf_token %}
    <div class="container w-25 bg-primary rounded-1">
        {% bootstrap_form form %}
        <button type="submit" class="btn btn-primary">Enviar</button>
        <button type="reset" class="btn btn-secondary">Borrar</button>
    </div>
</form>
{% endblock %}

Por ejemplo, vamos a la pestaña de Contacto, introducimos un nombre, un email y un mensaje. Cuando pulsemos el botón enviar se produce un POST, se pasa la información a la vista, los datos se almacenan en nombre, email y contenido y después se produce una redirección por GET a la misma página pasándole a la url el parámetro "valido" y se tiene que mostrar, si todo va bien "Información Enviada Correctamente".

mensaje enviado con exito

Envió de un email con los datos capturados al administrador del sistema.


Para enviar los datos de este formulario a, por ejemplo el administrador del sistema, tenemos que seguir los pasos para configurar el correo electrónico para Django que puedes encontrar aqui. (básicamente es añadir unos parámetros de configuración al archivo settings.py del proyecto)

Seria bueno que el correo electrónico se enviase en un hilo a parte para que el usuario no tuviese que estar esperando, mirando la pantalla mientras se realiza todo el proceso del envío del email. Para esto vamos a usar la librería threading.

Después modificamos el archivo views.py para que si el formulario es válido, acto seguido se envíe el correo y se emita un mensaje de confirmación:

PracticaDjango/Contacto/views.py: 

from django.shortcuts import render

# Para poder redireccionar a otras urls
from django.shortcuts import redirect

# API del formulario
from .forms import FormularioContacto

# Para enviar el correo electronico
from django.core.mail import send_mail, EmailMessage
from django.conf import settings

# Para que el correo electrónico funcione en un hilo aparte.
import threading

def enviar_email_en_hilo(correo):
    '''Enviará el email en un hilo aparte para que no se bloquee el programa.
    '''
    correo.send()

def contacto(request):
    formulario_contacto = FormularioContacto()
    
    # Si se ha hecho "POST" rescata la información del diccionario enviado.
    if request.method=="POST":
        # El método post devuelve un diccionario con los datos del formulario
        formulario_contacto = FormularioContacto(data=request.POST)
        # Si el formulario de contacto es válido, se han rellenado los campos obligatorios
        # y los campos están bien definidos.
        if formulario_contacto.is_valid():
            datos = formulario_contacto.cleaned_data
            nombre = datos.get("nombre")
            email = datos.get("email")
            contenido = datos.get("contenido")
            return redirect("/contacto/?valido")
    
        # Para enviar el correo electrónico
            email_from = settings.EMAIL_HOST_USER
            email_to = settings.EMAIL_DESTINATION

            # Esta sería una forma de enviarlo que ya vimos en el capitulo anterior.
            '''send_mail(
            f'Mensaje de {nombre}',
            f'{contenido} \nemail: {email}',
            email_from,
            [email_to],
            )'''

            # Para enviar el correo electrónico de otra forma
            correo = EmailMessage(
                'Mensaje desde APP Django',
                f'El usuario {nombre} con la dirección {email} escribe lo siguiente:\n\n{contenido}',
                email_from,
                [email_to],
                reply_to=[email] # Para responder al correo del que nos escribe.
            )
            try:            
                # Enviar el correo en un hilo aparte
                thread = threading.Thread(target=enviar_email_en_hilo, args=(correo,))
                thread.start()

                return redirect("/contacto/?valido")
            except:
                return redirect("/contacto/?novalido")           
            
                       
            # en get se pasan los parametros por la url usando ?

    
    return render(request, "Contacto/contacto.html", {'form':formulario_contacto})
Vamos a ver una explicación paso a paso de lo que está sucediendo:

1. Se utiliza el bloque `try-except` para capturar posibles excepciones durante el envío del correo electrónico.

2. Se crea un nuevo hilo utilizando el módulo `threading`. El objetivo de este hilo es llamar a la función `enviar_email_en_hilo` y pasarle el argumento `email`. La función `enviar_email_en_hilo` se encarga de realizar el proceso de envío de correo electrónico en un hilo aparte para evitar bloquear la ejecución del programa principal.

3. Se inicia el hilo llamando al método `start()` del objeto de hilo creado anteriormente. Esto hará que el hilo comience a ejecutar la función `enviar_email_en_hilo` con el argumento `email`.

4. Se utiliza la función `redirect()` para redirigir al usuario a una URL específica después de iniciar el proceso de envío del correo electrónico. En caso de que el proceso de envio tenga exito, se redirige al usuario a "/contacto/?valido". Si ocurre algún error durante el proceso de envío del correo, se redirige al usuario a "/contacto/?novalido".

Otra cosa es que luego el servidor de correo de un error al gestionar el correo, pero eso lo trataremos en otro capitulo, sobre como mostrar el error a través de mensajes.

En resumen, este código muestra una forma de enviar el correo electrónico en un hilo aparte para evitar bloqueos, y luego redirige al usuario a diferentes URLs dependiendo del resultado del proxeos de envío del mismo.



Creación de Formularios usando la clase ModelForm.


Una vez que hemos visto como crear formularios utilizando la clase Form, ahora vamos a ver como crear formularios a partir de un modelo de Django, usando la clase ModelForm. Para ello vamos a tener que volver a la aplicación "Blog" en donde estableceremos un sistema de comentarios que permitirá a los usuarios comentar los post que se publiquen. Para ello vamos a necesitar:

  • Un modelo "Comentario" para guardar los comentarios de los post.
  • Un formulario que permita a los usuarios enviar comentarios y manejar la validación de datos.
  • Una vista que procese el formulario y guarde un nuevo comentario en la base de datos.
  • Una lista de comentarios y un formulario para añadir un nuevo comentario que pueda ser incluido en la plantilla "detalle.html" que es la que muestra un post en concreto.

Creando un modelo para los comentarios.


Empezaremos construyendo un modelo para guardar los comentarios de los usuarios en los post.

Abre el archivo models.py de la aplicación Blog y añade el siguiente código:

PracticaDjango/Blog/models.py

# ...
class Comentario(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comentarios')
    autor = models.ForeignKey(User, on_delete=models.CASCADE, related_name='post_comentarios')
    email = models.EmailField()
    cuerpo = models.TextField(max_length=400)
    created = models.DateField(auto_now_add=True)
    updated = models.DateField(auto_now=True)
    activo = models.BooleanField(default=True)

    class Meta:
        ordering = ['created']
        indexes = [models.Index(fields=['created']),]
    
    def __str__(self):
        return f"Comentario de {self.autor} en {self.post}"
Este es el modelo "Comentario". Hemos añadido un campo ForeignKey para asociar cada comentario con un único post. Esta relación many-to-one está definida en el Modelo Comentario porque cada comentario pertenece a un único post mientras que un post puede tener multiples comentarios.

El atributo related_name te permite nombrar el atributo que utilizas para la relación desde el objeto relacionado de vuelta a este. Podemos recuperar la publicación de un objeto de Comentario usando comentarios.post y recuperar todos los comentarios asociados con un objeto post usando post.comentarios.all(). Si no defines el atributo related_name, Django utilizará el nombre del modelo en minúsculas, seguido por _set (es decir, comentarios_set) para nombrar la relación del objeto relacionado con el objeto del modelo donde se ha definido esta relación.

También hemos definido el campo Booleano "activo" para controlar el estado de un comentario. Este campo nos permitirá manualmente desactivar cualquier comentario que consideremos inapropiado usando el panel de administración. Por defecto el campo activo = True lo que nos indica que todos los comentario se mostrarán por defecto.

Hemos definido el campo "created" para establecer automáticamente la fecha de creación del post. Usando auto_now_add la fecha se grabará automaticamente cuando se cree el objeto. En la clase Meta del modelo hemos añadido ordering = ['created'] para ordenar los comentarios por orden cronológico y tambien hemos añadido un indice, lo que permitirá mejorar el rendimiento de la base de datos cuando se busque o filtre información basandose en este campo.

Como siempre que se modifica el archivo models.py es necesario hacer la migración.

(env) $ python manage.py makemigrations Blog
(env) $ python manage.py migrate

Añadiendo los comentarios al panel de administración.


A continuación añadiremos el modelo creado para que se pueda trabajar con él en el panel de administración de nuestro sitio. 

Abre el archivo admin.py de la aplicación Blog, importa el modelo Comentario y añade la siguiente clase ModelAdmin:

PracticaDjango/Blog/admin.py

# ...
# Importar del modelo tanto la categoría como el post
from .models import Categoria, Post, Comentario
# ...
@admin.register(Comentario)
class Comentario_admin(admin.ModelAdmin):
    list_display = ['autor', 'email', 'post', 'created', 'activo']
    list_filter = ['activo', 'created', 'updated']
    search_fields = ['autor', 'email', 'cuerpo']
Abre el navegador y  ve a http://127.0.0.1:8000/admin/  para comprobar que se puedan añadir nuevos comentarios y que todo funcione correctamente. 


Creando el formulario desde el modelo.


Ahora viene la parte que más nos interesa, que es como crear un formulario directamente desde un modelo. Necesitamos un formulario que permita a los usuarios comentar los post que se publiquen. Recuerda que Django tiene dos clases bases que permiten crear formularios: Form y ModelForm. Usamos Form para crear el formulario para que los usuarios contacten con nosotros dentro de la aplicación Contacto. Ahora vamos a usar ModelForm para usar el modelo "Comentario" y crear un formulario dinámicamente.

Crea un archivo llamado forms.py dentro de la aplicacion Blog y añade las siguientes líneas de código.

PracticaDjango/Blog/forms.py

from django import forms
from .models import Comentario

class ComentarioForm(forms.ModelForm):
    class Meta:
        model = Comentario
        fields = ['autor', 'email', 'cuerpo']

Para crear un formulario desde un modelo, tenemos que indicar desde que modelo queremos crear el formulario, lo cual indicamos en la clase Meta con model = Comentario. Django analizará los campos del modelo y en base a nuestras indicaciones construirá de forma dinámica el formulario.

Cada tipo de campo del modelo tiene su correspondencia en un campo por defecto en el formulario. Los atributos del los campos del modelo son tenidos en cuenta a la hora de validar los correspondientes campos del formulario. Por defecto Django crea un campo del formulario por cada campo existente en el modelo. Sin embargo, podemos decirle explícitamente a Django que campos queremos que se incluyan, usando el atributo fields dentro de la clase Meta o también que campos queremos excluir usando el atributo exclude. En el formulario ComentarioForm hemos especificado que se cree un formulario con los campos para autor, email y cuerpo. Estos serán los únicos campos que serán incluidos en el formulario.

Puedes encontrar más información sobre como crear formularios desde los modelos en:
https://docs.djangoproject.com/en/4.2/topics/forms/modelforms/.


Manejando los formularios basados en modelos en las vistas. (ModelForm)


Cuando creamos el formulario de Contacto utilizamos la misma vista para mostrar el formulario y enviar su contenido. Para ello usamos el método GET para mostrar el formulario y el método POST para enviar la información a la misma página. En este caso, mostraremos el formulario para comentar los post a través de la plantilla donde se renderizan los detalles de un post (detalle.html) y crearemos una vista separada para manejar los datos que envíe ese formulario. La nueva vista que procesará el formulario permitirá al usuario obtener los detalles del post una vez que los comentarios se hayan guardado en la base de datos.

Edita el archivo views.py de la aplicación Blog y añade el siguiente código:

PracticaDjango/Blog/views.py

from django.shortcuts import render, get_object_or_404, get_list_or_404

from .models import Post, Categoria, Comentario

# from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

from django.views.generic import ListView

# Importamos el formulario para los comentarios de cada post.
from .forms import ComentarioForm
from django.views.decorators.http import require_POST

# Create your views here.

# ....
@require_POST
def post_comentario(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    comentario = None
    # Se ha publicado un comentario
    form = ComentarioForm(data=request.POST)
    if form.is_valid:
        # creamos un objeto 'comentario' pero sin guardarlo en la base de datos.
        comentario = form.save(commit=False)
        # asignamos el post al comentario
        comentario.post = post
        comentario.save()
    return render(
        request,
        "Blog/comentario.html",
        {"post": post, "form": form, "comentario": comentario},
    )

Hemos definido la vista post_comentario que tomará como parámetros el request y el id del post. Usaremos esta vista para gestionar los datos del formulario. Estos datos esperamos que lleguen a esta vista mediante el método POST de HTML. Por eso usamos el decorador @require_POST que nos facilita Django para permitir solo request POST para esta vista. Django te permite restringir los métodos HTML permitidos para las vistas. Si intentas acceder a la vista con un método que no sea el permitido obtendrás un error HTML 405 (método no permitido).

En esta vista hemos implementado lo siguiente:

  1. Hemos obtenido el post al que añadir el comentario por su id utilizando el atajo get_object_or_404.
  2. Definimos la variable comentario con el valor inicial de None. Esta variable se usará para guardar el objeto comentario cuando se cree.
  3. Instanciamos el formulario con los datos enviados en el request a través del método POST y lo validamos usando el método is_valid(). Si el formulario no es válido se renderizará con los errores de validación.
  4. Si el formulario es válido se creara un nuevo objeto "comentario" llamando al método "save" del formulario y lo asignamos a una nueva variable "comentario" del siguiente modo. comentario = form.save(commit=False)
  5. El método save() crea una instancia del modelo al que el formulario está vinculado y lo guarda en la base de datos. Si lo llamas utilizando commit=False, se crea la instancia del modelo, pero no se guarda en la base de datos. Esto nos permite modificar el objeto antes de guardarlo definitivamente. IMPORTANTE: el metodo save() solo está disponible cuando utilizamos ModelForm pero no para las instancias creadas con Form ya que estas no están vinculadas a ningún modelo.
  6. Asignamos el post al comentario que hemos creado con "comentario.post = post"
  7. Grabamos el nuevo comentario en la base de datos llamando al método save().
  8. Finalmente renderizamos la plantilla "Blog/comentario.html", pasándole en el contexto el post, el formulario y los comentarios. 
Creemos el patrón URL para esta vista.

Edita el archivo urls.py de la aplicación Blog y añade el siguiente patrón:

PracticaDjango/Blog/urls.py

from django.urls import path
from . import views

app_name = 'Blog'

urlpatterns = [
# post views
# path('', views.lista_post, name='lista_post'),
path('', views.lista_post.as_view(), name='lista_post'),
path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.detalle_post, name='detalle_post'),
path('categoria/<int:categoria_id>/', views.categoria, name='categoria'),
path('<int:post_id>/comentario/', views.post_comentario, name='post_comentario'),
]
Como ya hemos creado las vistas que manejan el envío de los datos y también sus correspondientes Urls, nos vamos a poner con las plantillas.

Creando las plantillas para el formulario de comentario. 


Vamos a crear una plantilla para los comentarios que usaremos en dos sitios:

- En la plantilla "detalle.html" que está asociada a la vista "detalle_post" para permitir a los usuarios publicar comentarios.
- En la plantilla "comentario.html" que está asociada a la vista "post_comentario" para mostrar el formulario de nuevo si este contuviera errores.

Crearemos la plantilla para el formulario y usaremos la etiqueta {% include %} para incluir su código en las dos plantillas que hemos comentado anteriormente. Dentro de la aplicación Blog en el directorio /templates/Blog/ crea el archivo "form_comentario.html" y añade el siguiente código:

PracticaDjango/Blog/templates/Blog/form_comentario.html

<h2 style="margin-bottom: 4px;">Añade un nuevo comentario</h2>
<form action="{% url 'Blog:post_comentario' post.id %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <p><input type="submit" value="Añade comentario"></p>
</form>
En esta plantilla hemos construido la propiedad "action" de la etiqueta <form> de HTML dinámicamente usando la etiqueta {% url %}. Así el formulario será enviado a la vista "post_comentario" donde se procesará.         

En este mismo directorio creamos la plantilla "comentario.html" y añadimos el siguiente código.

PracticaDjango/Blog/templates/Blog/comentario.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Añade un comentario{% endblock %}

{% block content %}
    {% if comentario %}
    <div style="background-color: beige;">
        <h2>Tu comentario ha sido añadido</h2>
        <p><a href="{{post.get_absolute_url}}">Vuelve al Post</a></p>
    </div>
    {% else %}
        {% include "Blog/form_comentario.html" %}
    {% endif %}
{% endblock%}

Esta es la plantilla para la vista "post_comentario".  En esta vista esperamos que el formulario sea enviado usando el método Post. La plantilla contempla dos posibles escenarios: 

  • Si los datos enviados por el formularios son validos, es decir el formulario existe y no está vacío, la variable "comentario" contendrá el objeto comentario que fue creado, y se mostrará un mensaje de que el comentario ha sido añadido correctamente.
  • Por el contrario, si los datos enviados por el formulario no son correctos, la variable "comentario" será igual a None. En este caso se mostrará el formulario para introducir el comentario. Hemos usado la etiqueta {% include %} para incluir la plantilla "form_comentario.html" que creamos anteriormente.

Añadiendo comentarios a la vista "post_detalle".


Edita el archivo views.py y modifica el código de la vista "detalle_post" de la siguiente forma:

PracticaDjango/Blog/views.py

#...
def detalle_post(request, year, month, day, post):
    """Muestra todos los post"""
    post = get_object_or_404(
        Post,
        slug=post,
        created__year=year,
        created__month=month,
        created__day=day,
    )
    # Lista de comentarios activos para este post.
    comentarios = post.comentarios.filter(activo=True)
    # Formulario para que los usuarios comenten los post.
    form = ComentarioForm()
    return render(
        request,
        "Blog/detalle.html",
        {"post": post, "comentarios": comentarios, "form": form},
    )
#...

Revisemos el código que hemos añadido a la vista:

Hemos añadido una consulta a la base de datos para obtener todos los comentarios activos del post, de la siguiente forma:

comentarios = post.comentarios.filter(activo=True)

Esta búsqueda utiliza el objeto post. En vez de construir la busqueda desde el modelo "Comentario" directamente, aprovechamos el objeto post, para recuperar los comentarios. (related_name). También hemos creado una instancia del formulario de comentario con form = ComentarioForm().


Añadiendo comentarios a la plantilla de la vista detalle_post (detalle.html).

Necesitamos editar la plantilla Blog/detalle.html para añadir lo siguiente:

  • Mostrar el número total de comentarios que tiene un determinado post.
  • Mostrar una lista con los comentarios.
  • Mostrar el formulario para que los usuarios añadan un nuevo comentario.
Empezaremos añadiendo el número total de comentarios de un post.

Edita la plantilla y añade lo siguiente:

PracticaDjango/Blog/templates/Blog/detalle.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Detalle de un Post{% endblock %}

{% block content %}
<div class="container-fluid bg-white" style="margin-bottom: 150px;">
    <h1>{{ post.titulo }}</h1>

    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    
    {{ post.contenido|linebreaks }}

    {% with comentarios.count as total_comentarios %}
        <h2>
            {{ total_comentarios }} comentario{{ total_comentarios|pluralize }}
        </h2>
    {% endwith %}

    <!-- Contenedor para centrar el botón de regresar a la lista de posts -->
    <div class="d-flex justify-content-center align-items-center">
        <a href="{% url 'Blog:lista_post' %}" class="btn btn-primary">Regresar</a>
    </div>
</div>
{% endblock %}

Usamos el ORM de Django en la plantilla, ejecutando comentarios.count(). Date cuenta que el lenguaje de etiquetas de Django no contempla el usar parentesis para llamar a los métodos. La etiqueta {% with %} te permite asignar valores a una nueva variable que estará disponible en la plantilla hasta que se encuentre la etiqueta de cierre {% endwith %}.

Usamos el filtro de plantillas |pluralize para mostrar un sufijo a la palabra comentario dependiendo del valor de total_comentarios. Lo que hace es mostrar el sufijo "s" si el valor de total_comentarios es diferente de 1. Es decir, dependiendo del número de comentarios del post se mostrará 0 comentarios, 1 comentario, 2 comentarios y así sucesivamente.

Ahora, añadiremos la lista de comentarios activos para un post.

Edita de nuevo la plantilla y añade los siguiente cambios:

PracticaDjango/Blog/templates/Blog/detalle.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Detalle de un Post{% endblock %}

{% block content %}
<div class="container-fluid bg-white" style="margin-bottom: 150px;">
    <h1>{{ post.titulo }}</h1>

    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    
    {{ post.contenido|linebreaks }}

    {% with comentarios.count as total_comentarios %}
        <h2>
            {{ total_comentarios }} comentario{{ total_comentarios|pluralize }}
        </h2>
    {% endwith %}

    {% for comentario in comentarios %}
        <div class="comment">
            <p class="info">
                Comentario {{ forloop.counter }} de {{ comentario.autor }} - 
                {{ comentario.created }}
            </p>
            {{ comentario.cuerpo|linebreaks }}
        </div>
    
    {% empty %}
        <p>No hay comentarios aún.</p>
    
    {% endfor %}

    <!-- Contenedor para centrar el botón de regresar a la lista de posts -->
    <div class="d-flex justify-content-center align-items-center">
        <a href="{% url 'Blog:lista_post' %}" class="btn btn-primary">Regresar</a>
    </div>
</div>
{% endblock %}

Hemos añadido la etiqueta {% for %} para iterar a través de los comentarios. Si la lista de comentarios esta vacía mostraremos al usuario un mensaje informándole de que aun no hay comentarios para ese post, usando la etiqueta {% empty %}. También enumeramos los comentarios usando la variable {% forloop.counter }} que nos dice cual es la iteración en el bucle, es decir 1 , 2 , etc. Para cada post mostramos también la fecha de creación, quien es el autor y el contenido o cuerpo del mensaje.

Finalmente añadimos el formulario a la plantilla.

PracticaDjango/Blog/templates/Blog/detalle.html

{% extends "Proyecto_web_app/base.html" %}

{% block title %}Detalle de un Post{% endblock %}

{% block content %}
<div class="container-fluid bg-white" style="margin-bottom: 150px;">
    <h1>{{ post.titulo }}</h1>

    <p class="date">Publicado {{ post.autor }} por {{ post.updated }} </p>
    
    {{ post.contenido|linebreaks }}

    {% with comentarios.count as total_comentarios %}
        <h2>
            {{ total_comentarios }} comentario{{ total_comentarios|pluralize }}
        </h2>
    {% endwith %}

    {% for comentario in comentarios %}
        <div class="comment">
            <p class="info">
                Comentario {{ forloop.counter }} de {{ comentario.autor }} - 
                {{ comentario.created }}
            </p>
            {{ comentario.cuerpo|linebreaks }}
        </div>
    
    {% empty %}
        <p>No hay comentarios aún.</p>
    
    {% endfor %}

    <!-- Contenedor para centrar el botón de regresar a la lista de posts -->
    <div class="d-flex justify-content-center align-items-center">
        <a href="{% url 'Blog:lista_post' %}" class="btn btn-primary">Regresar</a>
    </div>
    {% include "Blog/form_comentario.html" %}
</div>
{% endblock %}

Para ver si todo funciona abre esta dirección en tu navegador (después de ejecutar el servidor) y haz click en el título de algún post que aun no tenga comentarios. Verás algo parecido a esto:

pagina detalle_post, con el formulario para incluir comentario

Rellena el formulario con datos que sean validos, baja un poco y dale al botón "añade comentario". Deberías ver la siguiente página.

Comentario añadido correctamente.


Haz clic en el enlace "vuelve al post". Deberías ser redireccionado a la vista de "post_detalles", que tendría que contener el comentario que acabas de añadir.

La pagina detalle_post con un comentario


Añade un segundo comentario al post. El comentario debería aparecer justo debajo del anterior en orden cronológico.

pagina detalle_post con dos comentarios



Si por último vas al panel de administración en 127.0.0.1:8000/admin/Blog/comentario/ verás todos los comentarios que hayas creado.