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.






No hay comentarios:

Publicar un comentario