domingo, 10 de diciembre de 2023

20.1.- Extendiendo el modelo User. (user model, modelo de usuario personalizado, mensajes, logearse con email )

Extendiendo el modelo User.

Cuando estas trabajando con cuentas de usuarios, a menudo te encuentras con que el modelo User del framework de autentificación de Django es más que suficiente en la mayor parte de los casos. Sin embargo, el modelo estandar de usuario de Django viene con un conjunto limitado de campos. 

Una forma sencilla de extender el modelo de Usuario es creando un modelo de perfil que contenga una relación uno a uno con el modelo de Usuario de Django, y cualquier campo adicional. Una relación uno a uno es similar a un campo ForeignKey con el parámetro unique=True. El lado inverso de la relación es una relación implícita uno a uno con el modelo relacionado en lugar de un gestor para múltiples elementos. Desde cada lado de la relación, se accede a un único objeto relacionado.

Vamos a suponer que queremos añadir al usuario dos campos nuevos, que no están reflejados en el modelo estándar como son la fecha de nacimiento y un avatar.

Pero antes, para que se vea mejor, vamos a crear una nueva vista que muestre una especie de panel de control cuando el usuario este logeado en su cuenta y donde pueda ver sus datos personales.

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

PracticaDjango/Autentificacion/views.py

# Para que solo se pueda acceder a la vista si el usuario esta logeado
from django.contrib.auth.decorators import login_required
#...

@login_required
def dashboard(request):
    return render(request, "Autentificacion/dashboard.html")

Con esto hemos creado la vista "dashboard" y añadido el decorador @login_required. Si el usuario está autentificado, se ejecuta la vista del decorador. Si no lo está le redirige al URL de inicio de sesión con la URL solicitada originalmente a través de un parámetro GET denominado "next". Al hacer esto, la vista de inicio de sesión redirige a los usuarios a la URL a la que intentaban acceder después de que se hayan logeado correctamente.

Recuerda que agregamos un elemento HTML <input> oculto llamdo "next" en la plantilla de template/registration/login.html precisamente con esta finalidad.

A continuación, necesitamos crear una plantilla para la vista "dashboard".

Crea un nuevo archivo llamado dashboard.html dentro de templates/Autentificacion y añade el siguiente código:

PracticaDjango/Autentificacion/templates/Autentificacion/dashboard.html

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

{% block title %}Panel de Control{% endblock %}

{% block content %}

<h1>Panel de Control</h1>
<p>Bienvenido a tu panel de control.</p>

{% endblock %}

Edita el archivo urls.py de la aplicación de autentificación y añade el nuevo código resaltado en azul.

PracticaDjango/Autentificacion/urls.py

from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    path("registrar/", views.registrar.as_view(), name="registrar"),
    path("", include('django.contrib.auth.urls')),
    path('', views.dashboard, name='dashboard'),
]

Lo que vamos a hacer ahora es añadir un icono de Bootstrap a la derecha de donde aparece el nombre de usuario, si este está logeado para que al pulsar en el se acceda directamente a esta vista. Para ello edita el archivo base.html de la aplicación Proyecto_web_app y añade este código:

PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/base.html

...
<body>
    <header>
        <h1 class="container-fluid text-center text-white bg-dark py-5">UnikGAME TIENDA VIRTUAL</h1>
        <div style="color: black; text-align: right; margin-right: 100px;">
            {% if user.is_authenticated %}
            Hola, {{user.username}}&nbsp;&nbsp;|&nbsp;&nbsp;
            <a href="{% url 'logout' %}">Cerrar Sesión</a>&nbsp; 
            | &nbsp;&nbsp; 
            <a href="{% url 'dashboard' %}">
            <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="bi bi-person" viewBox="0 0 16 16">
                <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664z"/>
            </svg>
            </a>
            {% else %}
            <a href="{% url 'login' %}">Login</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a
                href="{% url 'registrar' %}">Registrarse</a>
            {% endif %}
        </div>
        {% include "Proyecto_web_app/nav.html" %}
    </header>

    <div class="fondo">
...

Si estas logeado y accedes a esta dirección web con el navegador http://127.0.0.1:8000/cuenta/ deberías ver una página similar a esta:


Pagina con los datos del usuario o  panel de control

Bien, vamos ahora a añadir unas nuevas características al usuario como son la fecha de nacimiento y un avatar. Edita el archivo models.py de la aplicación de Autentificacion y añade el siguiente código:

PracticaDjango/Autentificacion/models.py

from django.db import models
from django.conf import settings

# Create your models here.


class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    fecha_nacimiento = models.DateField(blank=True, null=True)
    avatar = models.ImageField(upload_to="users/%Y/%m/%d/", blank=True)

    def __str__(self):
        return f"Profile of {self.user.username}"

Para mantener tu código, utiliza el método get_user_model() para recuperar el modelo de usuario y la configuración AUTH_USER_MODEL para referirte a él al definir la relación de un modelo con el modelo de usuario, en lugar de referirte directamente al modelo de usuario de autenticación. Puedes obtener más información al respecto en https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#django.contrib.auth.get_user_model.

El campo one-to-one 'user' se usará para asociar perfiles con usuarios. Con on_delete=models.CASCADE, forzamos la eliminación del objeto de perfil relacionado cuando se elimina un objeto de usuario.

El campo 'fecha_nacimiento' es un DateField. Hemos hecho este campo opcional con blank=True, y permitimos valores nulos con null=True.

El campo 'avatar' es un ImageField. Hemos hecho este campo opcional con blank=True. Un campo ImageField gestiona el almacenamiento de archivos de imagen. Valida que el archivo proporcionado sea una imagen válida, almacena el archivo de imagen en el directorio indicado con el parámetro upload_to y guarda la ruta relativa al archivo en el campo relacionado de la base de datos. Un campo ImageField se traduce por defecto a una columna VARCHAR(100) en la base de datos. Se almacenará una cadena en blanco si se deja vacío el valor.

En nuestra aplicación ya lo tenemos implementado, pues lo hemos usado varias veces para subir imágenes, puedes encontrar como configurar y subir imágenes en el capítulo "14.- Creación del primer servicio. Subir y visualizar una imagen en la base de datos."

Como siempre que se toca un archivo models.py hay que realizar las migraciones:

python manage.py makemigrations

python manage.py migrate


Edita el archivo admin.py de la aplicación Autentificacion y registra el modelo Perfil en el panel de administración de Django añadiendo el siguiente código:

PracticaDjango/Autentificacion/admin.py

from django.contrib import admin
from .models import Profile

# Register your models here.

@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
    list_display = ['user', 'fecha_nacimiento', 'avatar']
    raw_id_fields = ['user']

Ejecuta el servidor de desarrollo con el comando python manage,py runserver y navega a la dirección del panel de administración de Django "http://127.0.0.1:8000/admin". Luego haz clic en añadir un perfil. Deberías ver algo como esto:


página para insertar un avatar



Crea manualmente un objeto de Perfil para cada usuario que tengas registrado.

Lo siguiente es permitir a los usuarios que puedan editar su perfil en la página web.

Edita el archivo forms.py de la aplicación de Autentificacion y añade el siguiente código resaltado:

PracticaDjango/Autentificacion/forms.py

#...
from .models import Profile

#...
class UserEditForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['first_name', 'last_name', 'email']

class ProfileEditForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ['fecha_nacimiento', 'avatar']

Estos formularios permiten:

  • UserEditForm: Este formulario permite a los usuarios modificar su nombre, apellido y email que son atributos que ya existen en el perfil estandar del usuario (user) de Django.
  • ProfileEditForm: Permite editar los datos de su perfil que están guardados en el perfil personalizado que hemos creado. Podrán editar su fecha de nacimiento y subir una imagen para su avatar personalizado.
Edita el archivo views.py de la aplicación de Autentificación y añade el siguiente código resaltado:

PracticaDjango/Autentificacion/views.py

# ...
from .models import Profile

# ...
class registrar(View):
    def get(self, request):
        # creamos el formulario y se lo pasamos a la plantilla
        user_form = UserCreationWithEmailForm()
        return render(
            request, "Autentificacion/registro.html", {"user_form": user_form}
        )

    def post(self, request):
        user_form = UserCreationWithEmailForm(request.POST)
        # si el formulario es válido
        if user_form.is_valid():
            # Creamos un nuevo usuario y lo guardamos en la base de datos.
            new_user = user_form.save()
            # con .save() se guarda en la base de datos el hash de la contraseña y el usuario.
            # Al crear una cuenta si el registro
            # tiene exito el usuario se logea automáticamente y se redirecciona
            # a la pagina de inicio
            # Crea el objeto Profile
            Profile.objects.create(user=new_user)
            login(request, new_user)
            # una vez logueado hacemos la redirección a la pagina principal
            return redirect("Proyecto_web_app:home")
        return render(
            request, "Autentificacion/registro.html", {"user_form": user_form}
        )

Cuando el usuario se registra en el sitio, se creará un objeto Profile que será asociado con el objeto User creado.

Ahora, permitiremos a los usuarios que puedan cambiar su perfil.

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

PracticaDjango/Autentificacion/views.py

from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login
from .forms import LoginForm, UserCreationWithEmailForm, UserEditForm, ProfileEditForm
from django.contrib.auth.decorators import login_required

# Para crear la clase que nos servirá para registrar un nuevo usuario
from django.views.generic import View

# from django.contrib.auth.forms import UserCreationForm

from .models import Profile

#...
@login_required
def edit(request):
    try:
        profile = request.user.profile
    except Profile.DoesNotExist:
        profile = None

    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(
            instance=request.user.profile, data=request.POST, files=request.FILES
        )
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)
    return render(
        request,
        "Autentificacion/edit.html",
        {"user_form": user_form, "profile_form": profile_form},
    )

Hemos añadido la nueva vista de edición para permitir a los usuarios editar su información personal. Hemos agregado el decorador login_required a la vista porque solo los usuarios autentificados podrán editar sus perfiles. Para esta vista, usamos dos formularios de modelo: UserEditForm para almacenar los datos del modelo de usuario integrado y ProfileEditForm para almacenar los datos personales adicionales en el modelo personalizado de perfil. Para validar los datos enviados, llamamos al método is_valid() de ambos formularios. Si ambos formularios contienen datos válidos, guardamos ambos formularios llamando al método save() para actualizar los objetos correspondientes en la base de datos.

Agrega el siguiente patrón de URL al archivo urls.py de la aplicación de cuenta:

PracticaDjango/Autentificacion/urls.py

from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views

# from django.urls import reverse_lazy
# No usaremos app_name = "Autentificacion" porque dará problemas
# con el framework de autentificación

urlpatterns = [
    path("registrar/", views.registrar.as_view(), name="registrar"),
    path("", include('django.contrib.auth.urls')),
    path('', views.dashboard, name='dashboard'),
    path('edit/', views.edit, name='edit'),
]
Finalmente, crea una plantilla para esta vista en template/Autentificacion y llámalo edit.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/Autentificacion/edit.html

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

{% block title %}Edita tu Cuenta{% endblock %}

{% block content %}
<h1>Edita tu cuenta.</h1>
<p>Puedes editar tu cuenta usando el siguiente formulario:</p>
<form method="post" enctype="multipart/form-data">
    {{ user_form.as_p }}
    {{ profile_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Guardar Cambios"></p>
</form>

{% endblock %}

En el código anterior, hemos añadido enctype="multipart/form-data" al elemento HTML <form> para habilitar la carga de archivos. Utilizamos un formulario HTML para enviar tanto el formulario user_form como el formulario profile_form.

Abre la URL http://127.0.0.1:8000/cuenta/registrar/ y registra un nuevo usuario. Luego, inicia sesión con el nuevo usuario y abre la URL http://127.0.0.1:8000/cuenta/edit/. Deberías ver la siguiente página:


página para editar la cuenta de usuario


Ahora puedes agregar la información del perfil y guardar los cambios.

Vamos a editar la plantilla del panel para incluir enlaces a las páginas de edición de perfil y cambio de contraseña.

Abre la plantilla templates/Autentificacion/dashboard.html y agrega las siguientes líneas resaltadas en negrita:

PracticaDjango/Autentificacion/templates/Autentificacion/dashboard.html

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

{% block title %}Panel de Control{% endblock %}

{% block content %}

<h1>Panel de Control</h1>

<p>Bienvenido a tu panel de control. Tu puedes <a href="{% url 'edit' %}">
        editar tu pérfil</a> o <a href="{% url 'password_change' %}">
        cambiar tu contraseña</a>.</p>

{% endblock %}
Los usuarios ahora pueden acceder al formulario para editar su perfil desde el panel. Abre http://127.0.0.1:8000/cuenta/ en tu navegador y prueba el nuevo enlace para editar el perfil de un usuario. El panel debería parecer así ahora:


Contenido de la página de dashboard



Usando un modelo de usuario personalizado


Django también ofrece una manera de sustituir el modelo de Usuario con un modelo personalizado. La clase de Usuario debe heredar de la clase AbstractUser de Django, la cual provee la implementación completa del usuario por defecto como un modelo abstracto. Puedes leer más sobre este método en https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model.

Usar un modelo de usuario personalizado te dará más flexibilidad, pero también podría resultar en una integración más difícil con aplicaciones que interactúan directamente con el modelo de usuario de autenticación de Django.


Usando el framework de mensajes


Cuando los usuarios interactúan con la plataforma, hay muchos casos en los que puede que desees informarles sobre el resultado de acciones específicas. Django tiene un framework de mensajes integrado que te permite mostrar notificaciones de una sola vez a tus usuarios.

El framework de mensajes se encuentra en django.contrib.messages y está incluido en la lista default INSTALLED_APPS del archivo settings.py cuando creas nuevos proyectos usando python manage.py startproject. El archivo de configuración también contiene el middleware django.contrib.messages.middleware.MessageMiddleware en la configuración MIDDLEWARE.

El framework de mensajes proporciona una manera sencilla de añadir mensajes a los usuarios. Por defecto, los mensajes se almacenan en una cookie (en caso de que falle, se almacenan en la sesión) y se muestran y eliminan en la siguiente solicitud del usuario. Puedes utilizar el framework de mensajes en tus vistas importando el módulo 'messages' y añadiendo nuevos mensajes con atajos simples, de la siguiente manera:


from django.contrib import messages
messages.error(request, 'Something went wrong')


Puedes crear nuevos mensajes usando el método add_message() o cualquiera de los siguientes métodos abreviados:

success(): Mensajes de éxito para mostrar cuando una acción fue exitosa

info(): Mensajes informativos

warning(): Una falla aún no ha ocurrido pero puede ser inminente

error(): Una acción no fue exitosa o ocurrió una falla

debug(): Mensajes de depuración que serán eliminados o ignorados en un entorno de producción

Agreguemos mensajes al proyecto. El framework de mensajes se aplica de manera global al proyecto. Usaremos la plantilla base para mostrar cualquier mensaje disponible al cliente. Esto nos permitirá notificar al cliente sobre los resultados de cualquier acción en cualquier página.

Como hemos usado un context_proccesor cuando construimos la cesta de la compra solo tendremos que añadir el código a la vista. Ya que si lo agregaramos también a la plantilla se nos duplicará el mensaje. 

El marco de mensajes incluye el procesador de contexto django.contrib.messages.context_processors.messages, que agrega una variable de mensajes al contexto de la solicitud. Puedes encontrarlo en la lista context_processors en la configuración TEMPLATES de tu proyecto. Puedes usar la variable de mensajes en plantillas para mostrar todos los mensajes existentes al usuario.

Un procesador de contexto o context_proccesor es una función en Python que toma el objeto de solicitud como argumento y devuelve un diccionario que se agrega al contexto de la solicitud. 

Modifiquemos la vista de edición para usar el marco de mensajes.

Edita el archivo views.py de la aplicación de cuenta y agrega las siguientes líneas resaltadas en azul:

PracticaDjango/Autentificacion/views.py

#...
# Para mostrar mensajes
from django.contrib import messages

#...
@login_required
def edit(request):
    if request.method == "POST":
        user_form = UserEditForm(instance=request.user, data=request.POST)
        profile_form = ProfileEditForm(
            instance=request.user.profile, data=request.POST, files=request.FILES
        )
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, 'Actualización del Perfil realizada correctamente.')
        else:
            messages.error(request, 'Error actualizando tu Perfil.')
    else:
        user_form = UserEditForm(instance=request.user)
        profile_form = ProfileEditForm(instance=request.user.profile)
    return render(
        request,
        "Autentificacion/edit.html",
        {"user_form": user_form, "profile_form": profile_form},
    )
Si no hubiésemos utilizado el context_processor deberías añadir este código a la plantilla para renderizar el mensaje. Por ejemplo podrías abrir la plantilla templates/base.html de la aplicación de cuenta y agrega el siguiente código resaltado en negrita donde quieras que aparezca el mensaje:

Úsalo si no tienes context_proccesor en el proyecto

<ul class="messages">
        {% for message in messages %}
        <li class="{{ message.tags }}">
            {{ message|safe }}
            <a href="#" class="close">x</a>
        </li>
        {% endfor %}
</ul>

Un mensaje de éxito se genera cuando los usuarios actualizan exitosamente su perfil. Si alguno de los formularios contiene datos inválidos, en cambio se genera un mensaje de error.

Abre http://127.0.0.1:8000/cuenta/edit/ en tu navegador y edita el perfil del usuario. Deberías ver el siguiente mensaje cuando el perfil se actualiza exitosamente:


mensaje de exito en el cambio de perfil


Ingresa una fecha inválida en el campo de Fecha de nacimiento y envía el formulario nuevamente. Deberías ver el siguiente mensaje:


el mensaje de que el cambio de perfil no ha sido exitoso


Generar mensajes para informar a tus usuarios sobre los resultados de sus acciones es realmente sencillo. También puedes agregar fácilmente mensajes a otras vistas.

Puedes obtener más información sobre el marco de mensajes en https://docs.djangoproject.com/en/4.2/ref/contrib/messages/.


Construyendo un backend de autentificación personalizado


Django te permite autentificar usuarios contra diferentes fuentes. El ajuste AUTHENTICATION_BACKENDS incluye una lista de backends de autenticación disponibles en el proyecto. El valor predeterminado de este ajuste es el siguiente:

['django.contrib.auth.backends.ModelBackend']

El ModelBackend predeterminado autentifica a los usuarios contra la base de datos utilizando el modelo de Usuario (User) de django.contrib.auth. Esto es adecuado para la mayoría de los proyectos web. Sin embargo, puedes crear backends personalizados para autentificar a tus usuarios contra otras fuentes, como un directorio LDAP (Protocolo Ligero de Acceso a Directorios) u otro sistema.

Puedes obtener más información sobre la personalización de la autenticación en https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#other-authentication-sources.

Cada vez que se utiliza la función authenticate() de django.contrib.auth, Django intenta autentificar al usuario contra cada uno de los backends definidos en AUTHENTICATION_BACKENDS uno por uno, hasta que uno de ellos autentique con éxito al usuario. Solo si todos los backends fallan en autenticar, el usuario no será autentificado.

Django proporciona una manera sencilla de definir tus propios backends de autentificación. Un backend de autenticación es una clase que proporciona los siguientes dos métodos:

• authenticate(): Toma el objeto de solicitud (request) y las credenciales del usuario como parámetros. Debe devolver un objeto de usuario que coincida con esas credenciales si son válidas, o None en caso contrario. El parámetro request es un objeto HttpRequest, o None si no se proporciona a la función authenticate().

• get_user(): Toma un parámetro de ID de usuario y debe devolver un objeto de usuario.

Crear un backend de autentificación personalizado es tan sencillo como escribir una clase en Python que implemente ambos métodos. Creemos un backend de autentificación para permitir que los usuarios se autentifiquen en el sitio, utilizando su dirección de correo electrónico en lugar de su nombre de usuario.

Crea un archivo nuevo dentro del directorio de la aplicación de Autentificación y llámalo autentificacion.py. Añade el siguiente código a él:

PracticaDjango/Autentificacion/autentificacion.py

from django.contrib.auth.models import User

class EmailAuthBackend:
    """
    Autentificación usando una dirección de email
    """
    def authenticate(self, request, username=None, password=None):
        try:
            user = User.objects.get(email=username)
            if user.check_password(password):
                return user
            return None
        except (User.DoesNotExist, User.MultipleObjectsReturned):
            return None
    
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None


El código anterior es un backend de autentificación simple. El método authenticate() recibe un objeto de solicitud y los parámetros opcionales de nombre de usuario y contraseña. Podríamos usar diferentes parámetros, pero usamos nombre de usuario y contraseña para que nuestro backend funcione de inmediato con las vistas del marco de autenticación. El código anterior funciona de la siguiente manera:

• authenticate(): Se recupera el usuario con la dirección de correo electrónico proporcionada, y se verifica la contraseña utilizando el método check_password() integrado del modelo de usuario. Este método maneja el hash de la contraseña para comparar la contraseña dada con la contraseña almacenada en la base de datos. Se capturan dos excepciones diferentes del QuerySet: DoesNotExist y MultipleObjectsReturned. La excepción DoesNotExist se genera si no se encuentra ningún usuario con la dirección de correo electrónico proporcionada. La excepción MultipleObjectsReturned se genera si se encuentran varios usuarios con la misma dirección de correo electrónico. Modificaremos las vistas de registro y edición más tarde para evitar que los usuarios utilicen una dirección de correo electrónico existente.

• get_user(): Se obtiene un usuario a través del ID proporcionado en el parámetro user_id. Django utiliza el backend que autentifico al usuario para recuperar el objeto Usuario durante la sesión del usuario. pk es una abreviatura de clave primaria, que es un identificador único para cada registro en la base de datos. Cada modelo de Django tiene un campo que sirve como su clave primaria. De forma predeterminada, la clave primaria es el campo id generado automáticamente. La clave primaria también puede referirse como pk en el ORM de Django. Puedes encontrar más información sobre campos de clave primaria automática en https://docs.djangoproject.com/en/4.2/topics/db/models/#automatic-primary-key-fields.

Edita el archivo settings.py de tu proyecto y agrega el siguiente código:

PracticaDjango/PracticaDjango/settings.py

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'Autentificacion.autentificacion.EmailAuthBackend',
]
En la configuración anterior, mantenemos el ModelBackend predeterminado que se utiliza para autenticar con el nombre de usuario y la contraseña e incluimos nuestro propio backend de autenticación basado en correo electrónico, EmailAuthBackend.

Abre http://127.0.0.1:8000/cuenta/login/ en tu navegador. Recuerda que Django intentará autenticar al usuario contra cada uno de los backends, por lo que ahora deberías poder iniciar sesión sin problemas usando tu nombre de usuario o cuenta de correo electrónico.

La autenticación de las credenciales de usuario se verificará utilizando ModelBackend, y si no se devuelve ningún usuario, las credenciales se verificarán utilizando EmailAuthBackend.

Sin embargo tenemos que modificar algo en la vista de registro. El problema que me he encontrado es que cuando registras un nuevo usuario, los datos quedan guardados correctamente en la base de datos PERO cuando intenta logearlo automáticamente se genera el siguiente error:

ValueError at /cuenta/registrar/

You have multiple authentication backends configured and therefore must provide the `backend` argument or set the `backend` attribute on the user.

Para solucionarlo tenemos que indicarle a Django, pero solo en esta vista, explicitamente el backend que queremos usar. Para ello abrimos el archivo views.py de la aplicación de Autentitificación y añadimos a la vista registrar el siguiente código resaltado en azul:

PracticaDjango/Autentificacion/views.py

class registrar(View):
    def get(self, request):
        # creamos el formulario y se lo pasamos a la plantilla
        user_form = UserCreationWithEmailForm()
        return render(
            request, "Autentificacion/registro.html", {"user_form": user_form}
        )

    def post(self, request):
        user_form = UserCreationWithEmailForm(request.POST)
        # si el formulario es válido
        if user_form.is_valid():
            # Creamos un nuevo usuario y lo guardamos en la base de datos.
            new_user = user_form.save()
            # con .save() se guarda en la base de datos el hash de la contraseña y el usuario.
            # Al crear una cuenta si el registro
            # tiene exito el usuario se logea automáticamente y se redirecciona
            # a la pagina de inicio
            # Crea el objeto Profile
            Profile.objects.create(user=new_user)
            login(request, new_user, backend='django.contrib.auth.backends.ModelBackend')
            # una vez logueado hacemos la redirección a la pagina principal
            return redirect("Proyecto_web_app:home")
        return render(
            request, "Autentificacion/registro.html", {"user_form": user_form}
        )

El orden de los backends listados en la configuración AUTHENTICATION_BACKENDS es importante. Si las mismas credenciales son válidas para varios backends, Django se detendrá en el primer backend que autentique con éxito al usuario. Esto significa que, si tienes varios backends de autenticación y las credenciales proporcionadas son válidas para más de uno de esos backends, Django usará el primero de la lista que pueda autenticar al usuario con éxito y no continuará verificando los otros backends.


Prevención de usuarios que usan un correo electrónico existente


El modelo de usuario (User) del marco de autenticación no evita crear usuarios con la misma dirección de correo electrónico. Si dos o más cuentas de usuario comparten la misma dirección de correo electrónico, no podremos discernir qué usuario se está autenticando. Ahora que los usuarios pueden iniciar sesión utilizando su dirección de correo electrónico, debemos evitar que los usuarios se registren con una dirección de correo electrónico existente.

Ahora cambiaremos el formulario de registro de usuario para evitar que varios usuarios se registren con la misma dirección de correo electrónico.

Edita el archivo forms.py de la aplicación de Autentificacion, y añade las siguientes líneas resaltadas en azul a la clase UserCreationWithEmailForm:

PracticaDjango/Autentificacion/forms.py

#...
class UserCreationWithEmailForm(UserCreationForm):
    email = forms.EmailField(label='Correo electrónico', required=True)

    class Meta:
        model = User
        fields = ("username", "email", "password1", "password2")

    def clean_email(self):
        data = self.cleaned_data['email']
        if User.objects.filter(email=data).exists():
            raise forms.ValidationError('Este email ya está en uso.')
        return data
#...

Hemos añadido validación para el campo de correo electrónico que impide que los usuarios se registren con una dirección de correo electrónico existente. Construimos un conjunto de consultas (QuerySet) para buscar usuarios existentes con la misma dirección de correo electrónico. Verificamos si hay algún resultado con el método exists(). El método exists() devuelve Verdadero (True) si el conjunto de consultas contiene algún resultado, y Falso (False) en caso contrario.

IMPORTANTE:

Este método se ejecuta cuando el formulario es validado llamando a su método is_valid(). Puedes proporcionar un método clean_<nombrecampo>() para cualquiera de los campos de tu formulario para limpiar el valor o generar errores de validación específicos para un campo en particular. Los formularios también incluyen un método clean() general para validar el formulario completo, lo cual es útil para validar campos que dependen entre sí. En este caso, utilizamos la validación específica del campo clean_email() en lugar de anular el método clean() del formulario. Esto evita anular otras verificaciones específicas del campo que el ModelForm obtiene a partir de las restricciones establecidas en el modelo (por ejemplo, validar que el nombre de usuario sea único).

Ahora, agrega las siguientes líneas resaltadas en negrita a la clase UserEditForm:

PracticaDjango/Autentificacion/forms.py

class UserEditForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['first_name', 'last_name', 'email']
    
    def clean_email(self):
        data = self.cleaned_data['email']
        qs = User.objects.exclude(id=self.instance.id).filter(email=data)
        if qs.exists():
            raise forms.ValidationError(' Email ya en uso.')
        return data
En este caso, hemos añadido validación para el campo de correo electrónico que evita que los usuarios cambien su dirección de correo electrónico existente a una dirección de correo electrónico existente de otro usuario. Excluimos al usuario actual del conjunto de consultas (QuerySet). De lo contrario, la dirección de correo electrónico actual del usuario se consideraría una dirección de correo electrónico existente y el formulario no se validaría.


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


martes, 5 de diciembre de 2023

20.- Creación de un sistema de autenticación de usuarios.

En el capítulo anterior, exploramos cómo crear un widget para el carrito de compras. Sin embargo, para que nuestra aplicación funcione correctamente, es crucial permitir que los usuarios se autentifiquen. Esto es necesario para mantener los artículos agregados al carrito de cada usuario mediante el uso de sesiones. En consecuencia, cada usuario debe registrarse e iniciar sesión para poder utilizar esta funcionalidad.

En esta entrega, aprenderemos cómo implementar un sólido sistema de autenticación con enlaces clásicos de registro y acceso. En primer lugar, los usuarios podrán registrarse a través de un formulario, y sus datos quedarán almacenados en nuestra base de datos. Una vez completado este proceso, podrán acceder a la aplicación mediante un formulario de inicio de sesión, permitiéndoles mantener su sesión abierta para futuras interacciones.

Para llevar a cabo esto, crearemos una nueva aplicación dedicada exclusivamente a la gestión de la autenticación, que llamaremos "Autentificacion".

Empezamos creando una nueva aplicación que gestione esto llamada "autenticacion".


$ python manage.py startapp Autentificacion


Recuerda que debes añadir la nueva aplicación al proyecto, añadiendo el nombre de la aplicación dentro de INSTALLED_APPS dentro del archivo settings.py de la aplicación del proyecto "PracticaDjango". Edita este archivo y añade la lista resaltada dentro de la lista de INSTALLED_APPS:


PracticaDjango/PracticaDjango/settings.py

# Application definition

INSTALLED_APPS = [
    # Nuestras aplicaciones
    'Proyecto_web_app.apps.ProyectoWebAppConfig',
    'Servicios.apps.ServiciosConfig',
    'Blog.apps.BlogConfig',
    'Contacto.apps.ContactoConfig',
    'Tienda.apps.TiendaConfig',
    'Carro.apps.CarroConfig',
    'Autentificacion.apps.AutentificacionConfig',
    # Aplicaciones de terceros
    'django_bootstrap5',
    # Mapa del Sitio

Django busca las plantillas por orden de aparición en INSTALLED_APPS. La aplicación django.contrib.admin incluye plantillas de autentificación estandar que anularemos al registrar nuestra aplicación de Autentificación antes que esta. 

Ejecuta el siguiente comando para sincronizar la base de datos con los modelos de las aplicaciones predeterminadas incluidas en la configuración de INSTALLED_APPS:

python manage.py migrate


Usando el framework de autentificación de Django.


Ahora vamos a crear un sistema de autentificación de nuestro proyecto usando este framework.

Django viene con un marco de autenticación incorporado que puede manejar la autenticación de usuarios, sesiones, permisos y grupos de usuarios. El sistema de autenticación incluye vistas para acciones comunes de usuarios, como iniciar sesión, cerrar sesión, cambiar la contraseña y restablecer la contraseña.

El marco de autenticación se encuentra en django.contrib.auth y es utilizado por otros paquetes contrib de Django. Recuerda que ya utilizamos el marco de autentificación, cuando construimos la Aplicación del Blog, para crear un superusuario para la misma y acceder al sitio de administración.

Cuando creamos un nuevo proyecto de Django utilizando el comando startproject, el marco de autenticación está incluido en la configuración predeterminada de nuestro proyecto. Consiste en la aplicación django.contrib.auth y las siguientes dos clases de middleware encontradas en la configuración MIDDLEWARE de nuestro proyecto:

AuthenticationMiddleware: Asocia usuarios con peticiones utilizando sesiones.

SessionMiddleware: Maneja la sesión actual entre peticiones.

Middleware son clases con diferentes métodos que se ejecutan globalmente durante la fase de petición o respuesta de una página web y que veremos más adelante.

El framework de autentificación de Django también incluye los siguientes modelos que están definidos en django.contrib.auth.models:

  • User: Un modelo de usuario con unos campos básicos que son username, password, email, firstname, last_name y is_active.
  • Group: un grupo donde englobar y categorizar a los usuarios.
  • Permission: banderas para permitir a los usuarios o grupos realizar ciertas acciones.

El framework también incluye vistas y formularios predeterminados que usaremos más adelante.


Creando la vista del login.



Vamos a comenzar creando una vista que permita a los usuarios logearse en la página web. Crearemos una vista que permita a los usuarios hacer lo siguiente:

  • Presentaremos al usuario un formulario para loguearse.
  • Obtendremos el nombre de usuario y su contraseña cuando remita el formulario.
  • Autentificaremos el usuario verificando que exista en los datos guardados en la base de datos de la aplicación.
  • Comprobaremos que el usuario está activo.
  • Logearemos al usuario en la página web y comenzará a navegar en una sesión autentificada.

Empezaremos creando del formulario del login. 

PracticaDjango/Autentificacion/forms.py

from django import forms

class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)

Este formulario se usará para autenticar a los usuarios contra la base de datos. Ten en cuenta que se utiliza el widget PasswordInput para renderizar el elemento HTML de la contraseña. Esto incluirá type="password" en el HTML para que el navegador lo trate como un campo de contraseña.

Edita el archivo views.py de la aplicación de cuenta y agrega el siguiente código:

PracticaDjango/Autentificacion/views.py

from django.http import HttpResponse
from django.shortcuts import render
from django.contrib.auth import authenticate, login
from .forms import LoginForm

def user_login(request):
    if request.method == 'POST':
        form = LoginForm(request.POST)
        
        if form.is_valid():
            cd = form.cleaned_data
            user = authenticate(request, username=cd['username'], password=cd['password'])
            if user is not None:
                if user.is_active:
                    login(request, user)
                    return HttpResponse('Authenticated successfully')
                else:
                    return HttpResponse('Disabled account')
            else:
                return HttpResponse('Invalid login')
    else:
        form = LoginForm()
        return render(request, 'Autentificacion/login.html', {'form': form})
Esto es lo que hace básicamente la vista:

Cuando se llama a la vista user_login con una petición GET, se instancia un nuevo formulario de login con form = LoginFom(). El formulario se pasa luego a la plantilla.

Cuando el usuario envía el formulario a través del método POST, se realizan las siguientes acciones:

  • Se crea una instancia del formulario con los datos remitidos, con form = LoginForm(request.POST).
  • Se valida el formulario con is_valid(). Si no es válido, los errores del formulario se mostrarán más tarde en la plantilla. (Por ejemplo, si el usuario no rellena uno de los campos).
  • Si los datos enviados son válidos, el usuario se autentifica contra la base de datos utilizando el método authenticate(). Este método toma el objeto de solicitud, los parámetros de nombre de usuario y contraseña, y devuelve el objeto "user" si el usuario se ha autenticado correctamente, o None en caso contrario. Si el usuario no ha sido autentificado correctamente, se devuelve una HttpResponse con un mensaje de inicio de sesión no válido.
  •  Si el usuario se autentifica correctamente, se verifica el estado del usuario accediendo al atributo is_active. Este es un atributo del modelo de Usuario de Django. Si el usuario no está activo, se devuelve una HttpResponse con un mensaje de cuenta desactivada
  • Si el usuario está activo, se inicia sesión en el sitio. El usuario se establece en la sesión llamando al método login(). Se devuelve un mensaje de "Autenticación exitosa".
  • Si el usuario está activo, se inicia sesión en el sitio. El usuario se establece en la sesión llamando al método login(). Se devuelve un mensaje de "Autentificación realizada con éxito".

Toma nota de la diferencia entre authenticate() y login(): authenticate() 
verifica las credenciales del usuario y devuelve un objeto user si son 
correctas; login() establece al usuario en la sesión actual.

Ahora crearemos los parámetros URL para esta vista.

Crea un nuevo archivo llamado urls.py en el directorio de la aplicación Autentificación y añade el siguiente código:

PracticaDjango/Autentificacion/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("login/", views.user_login, name="login"),
]

Edita el archivo principal urls.py que está localizado dentro de la aplicación PracticaDjango y añade el código resaltado:

PracticaDjango/PracticaDjango/urls.py

from django.contrib import admin
from django.urls import path, include

from django.conf import settings
from django.conf.urls.static import static

from django.contrib.sitemaps.views import sitemap
from .sitemaps import BlogSitemap, StaticSitemap

sitemaps = {
    'static':StaticSitemap, #add StaticSitemap to the dictionary
    'blog':BlogSitemap #add DynamicSitemap to the dictionary
}

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('Proyecto_web_app.urls')),
    path('servicios/', include('Servicios.urls')),
    path('blog/', include('Blog.urls')),
    path('contacto/', include('Contacto.urls')),
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
    path('tienda/', include('Tienda.urls')),
    path('carro/', include('Carro.urls')),
    path('cuenta/', include('Autentificacion.urls')),
]
urlpatterns+=static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


Ahora ya podemos acceder a la vista user_login a través de la URL.

Vamos a crear una plantilla para esta vista. Crea el directorio templates/Autentificación dentro de la aplicación Autentificación y crea un archivo llamado login.html. Añade el siguiente código:


PracticaDjango/Autentificacion/templates/Autentificacion/login.html

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

<!--Cargamos de nuevo el contenido estático porque sino por algún motivo
no reconoce de nuevo la etiqueta bootstrap_form-->
{% load django_bootstrap5 %}

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

<!-- Definimos su contenido -->
{% block content %}

<!-- creamos el formulario -->
<form method="post" action="">
    <h1>Log-in</h1>
    <p>Por favor, use el siguiente formulario para logearse:</p>
    <form method="post">
        {% csrf_token %}
        <div class="container w-25 bg-primary rounded-1">
            {% bootstrap_form form %}
            <button type="submit" class="btn btn-primary">Log-in</button>
            <button type="reset" class="btn btn-secondary">Borrar</button>
        </div>
    </form>

</form>

{% endblock %}

Aún no tenemos ningún usuario en la base de datos, quitando el superusuario que creamos en capítulos anteriores al crear el Blog. Abre la dirección http://127.0.0.1:8000/admin/ en tu navegador. Accede al panel de administración con las credenciales que usaste cuando creaste el superusuario (en el capitulo de creación del blog, si no puedes crear uno con python manage.py createsuperuser). Al entrar verás los modelos de Usuarios y Grupos que utiliza el framework de autentificación de Django.

Tendrá un aspecto parecido a este:

el panel de administración de Django, usuarios y grupos

Haz click en la fila de usuarios y pulsa en añadir un nuevo usuario (+Add). En la siguiente pantalla Django te pedirá un nombre para el usuario y una contraseña.

formulario de usuario en el pandel de administración


Introduce los datos y pulsa el botón de guardar los datos (SAVE).

Luego en la siguiente pantalla "información personal" rellena los campos de nombre, apellido y correo electrónico.

Ahora abre la dirección http://127.0.0.1:8000/cuenta/login/ en tu navegador. Deberías ver la plantilla que creamos renderizada, que tendrá un aspecto parecido a este:

formulario de login

Vamos a ver si funciona. Para empezar introduce unas credenciales inválidas (un usuario inexistente o el que creaste antes con una contraseña errónea) y envía el formulario. 



mensaje de inicio de sesión no válido

Ahora introduce unas credenciales válidas (las del usuario que creaste anteriormente).  Deberías ver el siguiente mensaje de Autentificación correcta:


mensaje de exito en la autentificación


Usando las vistas de Autentificación de Django.


Django incluye varios formularios y vistas en el marco de autentificación que puedes usar de inmediato.

La vista de inicio de sesión que hemos creado es un buen ejercicio para comprender el proceso de autenticación de usuarios en Django. Sin embargo, en la mayoría de los casos puedes utilizar las vistas de autenticación predeterminadas de Django.

Django proporciona las siguientes vistas basadas en clases para manejar la autentificación. Todas ellas se encuentran en 'django.contrib.auth.views':


LoginView: Maneja un formulario de inicio de sesión e inicia sesión de un usuario.
LogoutView: Cierra sesión de un usuario.


Django proporciona las siguientes vistas para manejar cambios de contraseña:


PasswordChangeView: Maneja un formulario para cambiar la contraseña del usuario.
PasswordChangeDoneView: La vista de éxito a la que se redirige al usuario después de un cambio de contraseña exitoso.


Django también incluye las siguientes vistas para permitir a los usuarios restablecer su contraseña:


PasswordResetView: Permite a los usuarios restablecer su contraseña. Genera un enlace de un solo uso con un token y lo envía a la cuenta de correo electrónico del usuario.
PasswordResetDoneView: Informa a los usuarios que se les ha enviado un correo electrónico, incluyendo un enlace para restablecer su contraseña.
PasswordResetConfirmView: Permite a los usuarios establecer una nueva contraseña.
PasswordResetCompleteView: La vista de éxito a la que se redirige al usuario después de restablecer con éxito su contraseña.


Estas vistas pueden ahorrarte mucho tiempo al construir cualquier aplicación web con cuentas de usuario. Las vistas utilizan valores predeterminados que pueden ser reemplazados, como la ubicación de la plantilla a ser renderizada o el formulario a ser usado por la vista.


Puedes obtener más información sobre las vistas de autenticación integradas en https://docs.djangoproject.com/en/4.1/topics/auth/default/#all-authentication-views.


Vistas de Login y Logout.


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


PracticaDjango/Autentificacion/urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # login url anterior
    # path("login/", views.user_login, name="login"),
    # login / logout url
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]


En el código anterior, hemos comentado el patrón de URL para la vista user_login que creamos previamente. Ahora usaremos la vista LoginView del marco de autentificación de Django. También hemos añadido un patrón de URL para la vista LogoutView.

Crea un nuevo directorio dentro del directorio templates/ de la aplicación Autentificacion y llámalo registration. Este es el camino por defecto donde las vistas de autentificación de Django esperan que estén tus plantillas de autentificación.

El módulo django.contrib.admin incluye plantillas de autentificación que se utilizan para el sitio de administración, como la plantilla de inicio de sesión. Al colocar la aplicación Autentificacion en la parte superior de la configuración INSTALLED_APPS al configurar el proyecto, aseguramos que Django use nuestras plantillas de autentificación en lugar de las definidas en cualquier otra aplicación.

Crea un nuevo archivo dentro del directorio templates/registration/ y nómbralo login.html, luego agrega el siguiente código:

PracticaDjango/Autentificacion/templates/registration/login.html

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

{% load django_bootstrap5 %}

{% block title %}Log-in{% endblock %}

{% block content %}

<h1>Log-in</h1>
{% if form.errors %}
<p>
    Tu nombre de usuario y/o contraseña no coinciden.
    Por favor, inténtalo de nuevo.
</p>
{% else %}
<p>Por favor, use el siguiente formulario para log-in:</p>
{% endif %}
<div class="login-form">
    <form action="{% url 'login' %}" method="post">
        {% csrf_token %}
        <div class="container w-25 bg-primary rounded-1">
            {% bootstrap_form form %}
            <input type="hidden" name="next" value="{{ next }}" />
            <p><input type="submit" value="Log-in"></p>
    </form>
</div>

{% endblock %}

Este template de inicio de sesión es bastante similar al que creamos anteriormente. Django utiliza por defecto el formulario AuthenticationForm ubicado en django.contrib.auth.forms. Este formulario intenta autenticar al usuario y genera un error de validación si el inicio de sesión no es exitoso. Utilizamos {% if form.errors %} en la plantilla para verificar si las credenciales proporcionadas son incorrectas.

Hemos añadido un elemento HTML <input> oculto para enviar el valor de una variable llamada "next". Esta variable se proporciona a la vista de inicio de sesión si pasas un parámetro llamado "next" en la solicitud, por ejemplo, accediendo a http://127.0.0.1:8000/cuenta/login/?next=/cuenta/.

El parámetro "next" debe ser una URL. Si se proporciona este parámetro, la vista de inicio de sesión de Django redirigirá al usuario a la URL dada después de un inicio de sesión exitoso.

Ahora, crearemos la plantilla logged_out.html dentro del directorio templates/registration/ y añade un código similar a este:

PracticaDjango/Autentificacion/templates/registration/logged_out.html

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

{% block title %}Logged out{% endblock %}

{% block content %}

<h1>Logged out</h1>
<p>
    ¡Has cerrado la sesión correctamente!
    Puedes <a href="{% url 'login' %}">logearte de nuevo</a>.
</p>

{% endblock %}
Esta es la plantilla que Django mostrará al cerrar la sesión. 

Hemos añadido los patrones de URL y las plantillas para las vistas de inicio (login) y cierre de sesión (logout). Ahora, los usuarios pueden iniciar y cerrar sesión usando las vistas de autentificación de Django.


Registro de Usuarios y perfiles de Usuario.


Los usuarios del sitio ahora pueden iniciar sesión y  cerrar sesión. Sin embargo necesitamos crear una vista que permita a los visitantes crear una cuenta de usuario.


Registro de usuario


Creemos una vista sencilla para permitir el registro de usuarios en tu sitio web. Los campos que necesitaremos para crear un nuevo usuario son:

  • nombre de usuario
  • correo-electrónico
  • password1
  • password2 (para verificar que la contraseña se ha escrito correctamente)


Vamos a crear el formulario de registro usando UserCreationForm que nos va a permitir crear el formulario de registro una forma sencillísima. Lo vamos a tener que personalizar un poco ya que por defecto UserCreationForm no contiene el campo email y lo vamos a necesitar más tarde para restaurar la contraseña si el usuario la olvida.

Django tiene una forma sencilla de extender los formularios existentes para agregar nuevos campos. Lo primero que tenemos que hacer es crear un nuevo formulario que herede del "UserCreationForm" y luego agregar el campo adicional para el correo electrónico. 

Edita el archivo forms.py del directorio de la aplicación Autentificación que habiamos creado anteriormente y añade el código resaltado:

PracticaDjango/Autentificacion/forms.py

from django import forms

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User


class LoginForm(forms.Form):
    username = forms.CharField()
    password = forms.CharField(widget=forms.PasswordInput)


class UserCreationWithEmailForm(UserCreationForm):
    email = forms.EmailField(label='Correo electrónico', required=True)

    class Meta:
        model = User
        fields = ("username", "email", "password1", "password2")


Luego en la vista, importamos esa clase "UserCreationWithEmailForm" y creamos el formulario para crear usuario y lo renderizamos pasando por parámetro el formulario que acabamos de crear a la plantilla.


PracticaDjango/Autentificacion/views.py

from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login
from .forms import LoginForm, UserCreationWithEmailForm

# Para crear la clase que nos servirá para registrar un nuevo usuario
from django.views.generic import View

#...
    
class registrar(View):

    def get(self, request):
        # creamos el formulario y se lo pasamos a la plantilla
        user_form = UserCreationWithEmailForm()
        return render(request, "Autentificacion/registro.html", {"user_form":user_form})

    def post(self, request):
        user_form = UserCreationWithEmailForm(request.POST)
        # si el formulario es válido
        if user_form.is_valid():
            # Creamos un nuevo usuario y lo guardamos en la base de datos.
            new_user = user_form.save()
            # con .save() se guarda en la base de datos el hash de la contraseña y el usuario.
            # Al crear una cuenta si el registro
            # tiene exito el usuario se logea automáticamente y se redirecciona 
            # a la pagina de inicio
            login(request, new_user)
            # una vez logueado hacemos la redirección a la pagina principal
            return redirect('Proyecto_web_app:home')
        return render(request, "Autentificacion/registro.html", {"user_form":user_form})


La vista para crear un nuevo usuario es bastante sencilla. Cuando se llama a la vista por primera vez, usando el método GET, se crea una instancia del formulario y se traslada a la plantilla para que se renderice. 

Una vez enviado el formulario, a través del método POST, se creara una instancia del formulario. Pero sus campos ya no estarán vacíos puesto que recogen la información facilitada en request.POST. El formulario puede fallar dependiendo de lo que introduzca el usuario por eso usamos el "if form.is_valid()". Si no fuera válido se vuelve a renderizar el formulario vacío de nuevo. Pero si es correcto se creara una instancia del nuevo usuario (su nombre y contraseña) que se guardará en la base de datos y se realizará el login. Una vez logeado volverá a la página principal.

Aquí pararemos un momento para comentar, que Django no guarda directamente la contraseña que introduce el usuario. En su lugar las almacena en forma de "hashes". El hash es el resultado de transformar la clave dada en otro valor. Se utiliza una función hash para generar un valor de longitud fija según un algoritmo matemático. Repito, no se guarda la contraseña sino el valor que resulta de aplicarle un algoritmo matemático. 

Por defecto, Django utiliza el algoritmo de hash PBKDF2 con un hash SHA256 para almacenar todas las contraseñas. Sin embargo, Django no solo verifica contraseñas existentes hasheadas con PBKDF2, sino que también admite la verificación de contraseñas almacenadas con otros algoritmos como PBKDF2SHA1, argon2, bcrypt y scrypt.

La configuración PASSWORD_HASHERS define los algoritmos de hash de contraseñas que el proyecto Django admite y que se puede colocar en setttings.py del proyecto. La siguiente lista muestra los hashers predeterminados:


PASSWORD_HASHERS = [

'django.contrib.auth.hashers.PBKDF2PasswordHasher',

'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',

'django.contrib.auth.hashers.Argon2PasswordHasher',

'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',

'django.contrib.auth.hashers.ScryptPasswordHasher',

]


Django utiliza la primera entrada de la lista, en este caso PBKDF2PasswordHasher, para hashear todas las contraseñas. Los demás hashers pueden ser utilizados por Django para verificar contraseñas existentes.

Puedes obtener más información sobre cómo Django almacena contraseñas y los hashers de contraseñas incluidos en https://docs.djangoproject.com/en/4.2/topics/auth/passwords/.

Seguimos. Antes de poder ver como queda el formulario y los tres campos que contiene (nombre, contraseña y repetición de la contraseña) tenemos que editar el archivo urls.py de la aplicación Autentificación y añadir el siguiente parámetro de URL para su vista:


PracticaDjango/Autentificacion/urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # login url anterior
    # path("login/", views.user_login, name="login"),
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('registrar/', views.registrar.as_view(), name="registrar")
]

Finalmente crearemos una nueva plantilla en templates/Autentificacion llamada registro.html que será la encargada de renderizar el formulario. Tendrá el siguiente código:


PracticaDjango/Autentificacion/templates/Autentificacion/registro.html

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

{% load django_bootstrap5 %}

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

<!-- Definimos su contenido -->
{% block content %}

<!-- creamos el formulario -->
<form method="post" action="">
    {% csrf_token %}
    <div>&nbsp;</div>

    <div class="container w-50 bg-info rounded-1 pt-4">
        {% bootstrap_form user_form layout="horizontal" %}
        <!-- layout='horizontal' los nombres de los campos y sus etiquetas 
            se muestran en la misma línea -->
        <button type="submit" class="btn btn-primary">Registrate</button>
        <button type="reset" class="btn btn-secondary">Borrar</button>
    </div>

</form>

{% endblock %}

Ahora si ejecutamos el servidor y entramos en la dirección "http://127.0.0.1:8000/cuenta/registrar/" nos encontraremos lo siguiente:



formulario de registro de nuevo usuario



Lo que vamos a hacer a continuación es preparar un espacio en la parte superior derecha de la plantilla principal para que el usuario pueda logearse o registrarse, independientemente de la página en la que este. O si ya está logueado pueda cerrar su sesión.

Lo vamos a ubicar entre la etiqueta <h1> del titulo y la barra de navegación. Nos vamos para ello a la plantilla base

PracticaDjango/Proyecto_web_app/templates/Proyecto_web_app/base.html

#...
<body>
    <header>
        <h1 class="display-4 text-center text-white bg-dark py-3">
            UnikGAME
        </h1>
        <div style="color: black; text-align: right; margin-right: 100px;">
            {% if user.is_authenticated %}
            Hola, {{user.username}}&nbsp;&nbsp;|&nbsp;&nbsp;
            <a href="{% url 'logout' %}">Cerrar Sesión</a>
            {% else %}
            <a href="{% url 'login' %}">Login</a>&nbsp;&nbsp;|&nbsp;&nbsp;<a
                href=""{% url 'registrar' %}"">Registrarse</a>
            {% endif %}
        </div>
        {% include "Proyecto_web_app/nav.html" %}        
    </header>
    <!-- Parte cambiante de las plantillas -->
...
Si el usuario esta autentificado entonces se mostrará un mensaje de bienvenida con su nombre en la parte superior derecha de la pantalla y también la opción de cerrar sesión. Si el usuario no lo está entonces se mostrará un enlace para o bien poder logearse o registrarse en caso de que no lo haya hecho. 

Si el usuario no esta logeado le aparecerá este menú.

menu para logearse o registrarse


Y si ya esta registrado este otro.

menu bienvenida y cerrar sesión



Características Avanzadas.


Ahora que lo básico esta hecho vamos a pulir algunos detalles. Por ejemplo, cuando ahora el usuario se logea o cierra la aplicación, no se le redirige a ninguna página en concreto. Cuando se crea un nuevo usuario si que se le redirige a la página principal porque asi lo hemos programado nosotros. Podemos sin embargo hacer esto a través de las opciones de Django:

Edita el archivo settings.py del proyecto y añade el siguiente código:

PracticaDjango/PracticaDjango/settings.py

#...

# Redirección URL
LOGIN_REDIRECT_URL = 'Proyecto_web_app:home'
LOGIN_URL = 'login'
LOGOUT_URL = 'logout'


Hemos definido las siguientes configuraciones:


LOGIN_REDIRECT_URL: Indica a Django a qué URL redirigir al usuario después de un inicio de sesión exitoso si no hay un parámetro "next" presente en la solicitud.

LOGIN_URL: La URL a la que se redirige al usuario para iniciar sesión (por ejemplo, vistas que utilizan el decorador login_required).

LOGOUT_URL: La URL a la que se redirige al usuario para cerrar sesión, si no se ha especificado otra cosa.

Hemos empleado los nombres de las URLs que definimos previamente con el atributo name de la función path() en los patrones de URL. También se pueden usar URLs codificadas directamente en lugar de nombres de URL para estas configuraciones.


Cambiar o Resetear la contraseña de Usuario.


Necesitamos que los usuarios puedan cambiar sus contraseñas después de logearse en nuestra página. Vamos a hacer uso de las facilidades que nos proporciona Django. 

Abre el archivo urls.py de la aplicación Autentificacion y añade los siguientes parámetros de URL:

PracticaDjango/Autentificacion/urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

from django.urls import reverse_lazy

urlpatterns = [
    # login url anterior
    # path("login/", views.user_login, name="login"),
    path("login/", auth_views.LoginView.as_view(), name="login"),
    path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("registrar/", views.registrar.as_view(), name="registrar"),
    # cambio de contraseña
    path(
        "password-change/",
        auth_views.PasswordChangeView.as_view(
            success_url=reverse_lazy("Autentificacion:password_change_done")
        ),
        name="password_change",
    ),
    path(
        "password-change/done/",
        auth_views.PasswordChangeDoneView.as_view(),
        name="password_change_done",
    ),
]


La vista PasswordChangeView manejará el formulario para cambiar la contraseña, y la vista PasswordChangeDoneView mostrará un mensaje de éxito después de que el usuario haya cambiado exitosamente su contraseña. 

INCISO. Si hubieramos usado app_name = "Autentificacion" en urls.py, cosa que no hemos hecho ni recomiendo hacer,  tendríamos que haber escrito la ruta como:

# Primero importar reverse_lazy
from django.urls import reverse_lazy

# Y luego espeficicar así el parámetro URL
path(
        "password-change/",
        auth_views.PasswordChangeView.as_view(
            success_url=reverse_lazy("Autentificacion:password_change_done")
        ),
        name="password_change",
    ),
app_name = "Autentificacion", provoca que nuestra URL use un namespace de la aplicación llamado "Autentificacion". PasswordChangeView no espera que haya un namespace de la aplicación cuando busca la vista nombrada como password_change_done. Esto provocaría un error cuando una vez enviado el formulario de cambio de contraseña se intentara mostrar la página de que la contraseña se ha cambiado con éxito. Sin embargo podemosdefinir explicitamente el atributo success_url dentro de PasswordChangeView. El parámetro success_url espera la URL a la que se redirigirá una vez que se haya cambiado la contraseña correctamente.

La función reverse_lazy("Autentificacion:password_change_done") busca la URL con nombre password_change_done que está definida en el espacio de nombres (namespace) llamado Autentificacion.

Puedes encontrar más información sobre namespace en: https://docs.djangoproject.com/en/4.2/topics/http/urls/#url-namespaces-and-included-urlconfs


SEGUIMOS. Creemos, ahora,  una plantilla para cada vista. Crea un nuevo archivo dentro del directorio templates/registration/ con el nombre de password_change_form.html y añade este código:


PracticaDjango/Autentificacion/templates/registration/password_change_form.html

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

{% load django_bootstrap5 %}

{% block title %}Cambia tu contraseña{% endblock %}

{% block content %}
<h1>Cambia tu contraseña</h1>
<p>Usa el siguiente formulario para cambiar la contraseña.</p>

<div class="login-form">
    <form action="" method="post">
        {% csrf_token %}
        <div class="container w-25 bg-primary rounded-1">
            {% bootstrap_form form %}
            <p><input type="submit" value="Cambiar"></p>
    </form>
</div>
{% endblock %}

La plantilla password_change_form.html incluye el formulario para cambiar la contraseña. 

Ahora crearemos otro archivo en el mismo directorio y lo llamaremos password_change_done.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_change_done.html

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

{% block title %}Contraseña Cambiada{% endblock %}

{% block content %}
<h1>Contraseña Cambiada</h1>
<p>Tu contraseña se ha cambiado con éxito.</p>
{% endblock %}
La plantilla password_change_done.html contiene únicamente el mensaje de éxito que se mostrará cuando el usuario haya cambiado exitosamente su contraseña. Abre http://127.0.0.1:8000/cuenta/password-change/ en tu navegador. Si no has iniciado sesión, el navegador te redireccionará a la página de inicio de sesión. Después de autentificarte correctamente, verás la siguiente página para cambiar la contraseña:

formulario para cambiar contraseña


Si rellenas el formulario con tu contraseña actual y tu nueva contraseña y le das al botón de cambiar debería salirte la plantilla en la que te dicen que la contraseña se ha cambiado correctamente. Logeate y cierra sesión de nuevo usando tu nueva contraseña para comprobar que todo funciona como debiera.


Vista para resetear la contraseña.


¿Y si se nos olvida la contraseña? Pues que tendremos que resetearla para poder introducir una nueva. Vamos con ello. Edita el archivo urls.py de la aplicación de Autentificación y añade el siguiente código al final del archivo dentro de urlpatterns:

PracticaDjango/Autentificacion/urls.py

#...
# reset password urls
    path(
        "password-reset/", 
        auth_views.PasswordResetView.as_view(), 
        name="password_reset",
    ),
    path(
        "password-reset/done/",
        auth_views.PasswordResetDoneView.as_view(),
        name="password_reset_done",
    ),
    path(
        "password-reset/<uidb64>/<token>/",
        auth_views.PasswordResetConfirmView.as_view(),
        name="password_reset_confirm",
    ),
    path(
        "password-reset/complete/",
        auth_views.PasswordResetCompleteView.as_view(),
        name="password_reset_complete",
    ),

Añade un nuevo archivo en el directorio templates/registration de la aplicación de autentificación y llámalo password_reset_form.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_reset_form.html

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

{% block title %}Resetea tu contraseña{% endblock %}

{% block content %}
<h1>¿Olvidaste tu contraseña?</h1>
<p>Introduce tu dirección de e-mail para obtener una nueva.</p>
<form method="post">
    {{ form.as_p }}
    <p><input type="submit" value="Enviar e-mail"></p>
    {% csrf_token %}
</form>
{% endblock %}
Ahora crea otro archivo en el mismo directorio y llámalo password_reset_email.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_reset_email.html

Alguien ha solicitado restablecer la contraseña de este email: {{ email }}. 
Sigue el siguiente enlace:
{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}
Tu nombre de usuario, en caso de que lo hayas olvidado es: {{ user.get_username }}
La plantilla password_reset_email.html se usará para renderizar el correo que se enviará a los usuarios para restablecer su contraseña. Incluye un token de restablecimiento proporcionado por la vista.

Crea otro archivo en el mismo directorio y llámalo password_reset_done.html. Añádele el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_reset_done.html

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

{% block title %}Restablece tu contraseña{% endblock %}

{% block content %}

<h1>Restablecer tu contraseña.</h1>
<p>Te hemos enviado instrucciones al correo electrónico para restablecer la contraseña</p>
<p>Si no lo has recibido, asegúrate de haber usado el correo electrónico con el que 
    te registraste.</p>
{% endblock %}
Crea otra plantilla en el mismo directorio y llámalo password_reset_confirm.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_reset_confirm.html

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

{% block title %}Restable tu contraseña{% endblock %}

{% block content %}

<h1>Restablece tu contraseña</h1>
{% if validlink %}
<p>Por favor, introduce tu nueva contraseña dos veces:</p>
<form method="post">
    {{ form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Cambiar mi contraseña" /></p>
</form>
{% else %}
<p>El enlace para restablecer la contraseña no era válido, posiblemente porque ya
    ha sido usado. Solicita un nuevo restablecimiento de contraseña.</p>
{% endif %}

{% endblock %}
En esta plantilla, confirmamos si el enlace para restablecer la contraseña es válido cuando se haga click en el enlace proporcionado. La vista PasswordResetConfimView comprueba la validez del token proporcionado en la URL y pasa la variable validlink a la plantilla. Si el enlace es válido, la contraseña del usuario se muestra en el formulario de reinicio. Los usuarios solo podrán restablecer la contraseña si tienen un enlace válido para ello.

Crea otra plantilla en el mismo directorio y llámala password_reset_complete.html. Añade el siguiente código:

PracticaDjango/Autentificacion/templates/registration/password_reset_complete.html

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

{% block title %}Contraseña restablecida{% endblock %}

{% block content %}

<h1>Contraseña registrada.</h1>
<p>Tu contraseña ha sido registrada. Puedes <a href="{% url 'login'  %}">
    logearte ahora</a></p>

{% endblock %}
Finalmente, edita la plantilla templates/registration/login.html de la aplicación Autentificación y añade las líneas resaltadas.

PracticaDjango/Autentificacion/templates/registration/login.html

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

{% load django_bootstrap5 %}

{% block title %}Log-in{% endblock %}

{% block content %}

<h1>Log-in</h1>
{% if form.errors %}
<p>
    Tu nombre de usuario y/o contraseña no coinciden.
    Por favor, inténtalo de nuevo.
</p>
{% else %}
<p>Por favor, use el siguiente formulario para log-in:</p>
{% endif %}
<div class="login-form">
    <form action="{% url 'login' %}" method="post">
        {% csrf_token %}
        <div class="container w-25 bg-primary rounded-1">
            {% bootstrap_form form %}
            <input type="hidden" name="next" value="{{ next }}" />
            <p><input type="submit" value="Log-in"></p>
            <!-- {{ form.as_p }} -->
    </form>
    <p>
        <a href="{% url 'password_reset' %}" style="color:white">
            ¿Olvidaste tu contraseña?
        </a>
    </p>
</div>

{% endblock %}
Ahora abre la dirección htpp://127.0.01:8000/cuenta/login en tu navegador. La página del login debería incluir un link para poder resetear la contraseña.

la página de login incluyendo un enlace para resetear la contraseña


Haz clic en el link ¿Olvidaste tu contraseña?. Deberías ver la siguiente página:


formulariio para introducir el correo y resetear la contraseña



En este punto, necesitamos añadir un Simple Mail Transfer Protocol dentro del archivo de configuración del Proyecto (settings.py) para que Django pueda enviar Emails. Aprendimos como hacerlo en el capitulo "10.- Formulario de contacto. Envío de Emails y variables de entorno". Sin embargo durante el desarrollo de la aplicación, puedes configurar Django para que envíe los correos a la salida estandar utilizando este apartado dentro del archivo de configuración settings.py del proyecto:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Ahora vuelve al navegador e introduce la dirección de correo de un usuario que tengas creado. (Por eso pusimos el campo email cuando el usuario se registra). Luego haz clic en el botón Enviar Email. Deberías ver la siguiente página:


mensaje de correo enviado para restaurar contraseña



Echa un vistazo al shell de la consola donde estás ejecutando el servidor de desarrollo de Django. Verás el email que se ha enviado al usuario para restablecer la contraseña:


token para restablecer la contraseña



El email es renderizado usando la plantilla password_reset_email.html que creamos previamente. La URL para restablecer la contraseña incluye un token que ha sido generado por Django. Copia la URL desde el email, que debería tener un aspecto similar a http://127.0.0.1:8000/cuenta/reset/Mg/bypfqo-d8b1f54efabda9254fa3f37df2b967da/ y ábrelo en tu navegador. Debería ver la siguiente página:


formulario para introducir la nueva contraseña



La página para introducir una nueva contraseña usa la plantilla password_reset_confirm.html. Introduce una nueva contraseña y haz clic en Cambiar mi contraseña. Django creará un nuevo hash de la contraseña y la guardará en la base de datos. Verás la siguiente página indicando que la operación ha sido llevada a cabo con exito:


contraseña registrada con exito



Puede volver a logearte usando la nueva contraseña.

Cada token para establecer una nueva contraseña solo se puede utilizar una vez. Si abres el enlace que recibiste nuevamente, recibirás un mensaje que indica que el token no es válido.

Hemos integrado las vistas del marco de autenticación de Django en el proyecto. Estas vistas son adecuadas para la mayoría de los casos. Sin embargo, puedes crear tus propias vistas si necesitas un comportamiento diferente.

Django proporciona patrones de URL para las vistas de autenticación que son equivalentes a los que acabamos de crear. Reemplazaremos los patrones de URL de autenticación con los proporcionados por Django.

Comenta los patrones de URL de autenticación que agregaste al archivo urls.py de la aplicación de Autentificación e incluye django.contrib.auth.urls en su lugar, de la siguiente manera. ¡Verás que reducido se queda el archivo!. El nuevo código está resaltado en azul:

PracticaDjango/Autentificacion/urls.py

from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views

# from django.urls import reverse_lazy
# No usaremos app_name = "Autentificacion" porque dará problemas
# con el framework de autentificación

urlpatterns = [
    # login url anterior
    # path("login/", views.user_login, name="login"),
    # path("login/", auth_views.LoginView.as_view(), name="login"),
    # path("logout/", auth_views.LogoutView.as_view(), name="logout"),
    path("registrar/", views.registrar.as_view(), name="registrar"),
    # # cambio de contraseña
    # path(
    #     "password-change/",
    #     auth_views.PasswordChangeView.as_view(),
    #     name="password_change",
    # ),
    # path(
    #     "password-change/done/",
    #     auth_views.PasswordChangeDoneView.as_view(),
    #     name="password_change_done",
    # ),
    # # reset password urls
    # path(
    #     "password-reset/", 
    #     auth_views.PasswordResetView.as_view(), 
    #     name="password_reset",
    # ),
    # path(
    #     "password-reset/done/",
    #     auth_views.PasswordResetDoneView.as_view(),
    #     name="password_reset_done",
    # ),
    # path(
    #     "password-reset/<uidb64>/<token>/",
    #     auth_views.PasswordResetConfirmView.as_view(),
    #     name="password_reset_confirm",
    # ),
    # path(
    #     "password-reset/complete/",
    #     auth_views.PasswordResetCompleteView.as_view(),
    #     name="password_reset_complete",
    # ),
    path("", include('django.contrib.auth.urls')),
]

¡Se ha quedado reducido todo a dos líneas reales de path!

Y hasta aquí la creación de un sistema de autentificación de usuarios en la aplicación.

Puedes encontrar el código del capítulo en el siguiente enlace de GitHub.