Modelo de Usuarios personalizado en Django

Django es un Framework para el desarrollo de Aplicaciones Web en Python, que incluye un modelo de autenticación y gestión de usuarios que podemos extender para adaptar a nuestras necesidades (tanto para usuarios admin, como no admin), algo que no sólo es habitual, sino también una práctica recomendada. En este Post vamos a ver cómo extender el modelo de Usuarios proporcionado por Django, añadiendo atributos, personalizando sus páginas, así como otros detalles como el envío de email (ej: reseteo de contraseña) y la configuración de secretos desde variables de entorno o desde un fichero .env (evitando hardcodear datos sensibles o configuraciones variables en el settings.py y registrarlos en el historial de git).

Continuando con la serie de Posts sobre Python y Django, en esta ocasión vamos a ver paso a paso cómo extender el modelo de usuarios proporcionado por Django (extendiendo la clase AbstractUser) para personalizarlo y adaptarlo a nuestras necesidades (añadir un nuevo atributo así como crear y personalizar los formularios), incluyendo detalles como el envío de emails y la gestión de la configuración desde variables de entorno o desde un fichero .env, todo esto a través de un ejemplo, como siempre disponible en un repo público de GitHub: GitHub – ElWillieES – django-custom-users

Introducción

Es una buena práctica comenzar los nuevos proyectos utilizando el modelo de usuarios personalizado de Django, en lugar de el modelo de usuarios por defecto que vimos en el anterior Post, pues en caso contrario, si partimos del modelo por defecto y más adelante lo deseamos poder cambiar o extender, resultaría bastante más complicado que si ya partimos desde el comienzo de un modelo personalizado.

Principalmente hay dos aproximaciones para extender el modelo de usuarios de Django:

  • Extender la clase AbstractBaseUser. Es más complicado y requiere escribir una mayor cantidad de código.
  • Extender la clase AbstractUser. Es más sencillo, requiere escribir menos código, y es la forma que vamos a seguir en este Post.

Vamos a ver la forma de implementarlo, mediante la extensión de la clase AbstractUser, configurándola para usarla en el Panel de Administración de Django, así como proporcionando también las páginas de login, logout, signup, cambio de contraseña, y reseteo de contraseña, para que los usuarios no admin pueden iniciar o cerrar sesión, registrarse, cambiar su password, o recuperarla en caso de olvido.

Esto implica que además necesitaremos enviar emails (ej: para recuperar la contraseña), y dado que la configuración del servicio SMTP requiere almacenar datos sensibles (credenciales), vamos a ver también como gestionar la configuración del fichero settings.py desde variables de entorno o desde un fichero .env.

Si estás comenzando con Python, quizás te interese leer primero los anteriores Post:

Dicho esto, comenzamos.

Personalizar el modelo de Usuarios extendiéndolo

Lo primero que vamos a realizar es crear y preparar el nuevo Proyecto de Python en PyCharm, en línea con lo que hemos hecho en los anteriores Posts:

  • Crear nuestro Proyecto Python en PyCharm (django-custom-users), con su propio Virtual Environment (en mi caso usando Python 3.9).
  • Creamos la carpeta app, la carpeta app/templates, y copiamos a la raiz algunos ficheros, como el Dockerfile, docker-compose.yml, requirements.txt, etc, y los adaptamos a este nuevo Proyecto en los casos que aplica.
  • Instalamos las librerías definidas en el requirements.txt
  • Dentro del directorio app, desde línea de comandos (ventana Terminal de PyCharm), creamos un Proyecto Django (custom_users) y una Aplicación Django (accounts).
django-admin startproject custom_users .
python manage.py startapp accounts
  • En el menú Run -> Configure de PyCharm, configuraremos que se arranque el servidor Web de Django, al ejecutar o depurar el Proyecto de PyCharm. También establecemos el Working directory.
  • A continuación instalar la librería Coverage.py para poder ejecutar las pruebas unitarias y obtener el informe de cobertura. No lo vamos a añadir al requirements.txt, en su lugar, nos vamos a limitar a instalarlo en el Virtual Environment de PyCharm (pestaña Python Packages).
  • Seguidamente, editamos el fichero settings.py del Proyecto Django para:
    • Referenciar la Aplicación accounts que acabamos de crear en la opción INSTALLED_APPS.
    • Especificar en la opción TEMPLATES que deseamos utilizar una carpeta única (app/templates que acabamos de crear) para almacenar las Plantillas de todo el Proyecto.
    • Añadir al final del fichero la opción AUTH_USER_MODEL para decirle a Django que queremos utilizar un modelo de Usuarios personalizado (CustomUser).
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'accounts.apps.AccountsConfig',
]
...
TEMPLATES = [
    {
        ...
        'DIRS': [BASE_DIR / "templates"],
        ...
]
...
AUTH_USER_MODEL = "accounts.CustomUser"

Ahora vamos a extender el modelos de Usuarios, para lo cual, editaremos el fichero models.py y extenderemos la clase AbstractUser para añadirle un atributo (department), especificando las opciones null y blank:

  • null es una opción que indica si se permiten valores null o no en la base de datos.
  • blank es una opción que indica si se permite que el campo pueda quedar vacío o no en un formulario.
from django.contrib.auth.models import AbstractUser
from django.db import models


class CustomUser(AbstractUser):
    department = models.CharField(null=True, blank=True, max_length=50)

Lo siguiente es extender los Formularios, en particular las clases UserCreationForm y UserChangeForm, para sobrescribir los campos que incluyen por defecto. Para ello, creamos un nuevo fichero forms.py con el siguiente contenido.

from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm):
        model = CustomUser
        fields = UserCreationForm.Meta.fields + ("department",)

class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = UserChangeForm.Meta.fields

También necesitamos modificar el fichero admin.py para que el Panel de Administración de Django utilice el nuevo modelo extendido de Usuarios y sus formularios.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser


class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = [
        "email",
        "username",
        "department",
        "is_staff",
    ]
    fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("department",)}),)
    add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("department",)}),)


admin.site.register(CustomUser, CustomUserAdmin)

Hecho esto, ya sólo nos queda realizar las siguientes tareas, que podemos hacer desde la ventana Terminal de PyCharm posicionados en el directorio app:

  • Crear las migraciones del nuevo modelo de Usuarios
  • Ejecutar las migraciones
  • Crear un superusuario
python manage.py makemigrations accounts
python manage.py migrate
python manage.py createsuperuser

Con esto ya habríamos acabado, y podríamos ejecutar y probarlo, para ver el resultado. Si lo hacemos y accedemos al Panel de Administración de Django, podremos ver que ya aparece el nuevo campo que hemos añadido.

Igualmente, si accedemos al formulario de edición (o al de creación de nuevo usuario), para modificar un usuario existente, tendremos disponible el nuevo campo para editar su valor y guardar los cambios.

Autenticación de Usuarios

Editaremos el fichero settings.py del Proyecto Django, para añadir las opciones LOGIN_REDIRECT_URL y LOGOUT_REDIRECT_URL, que especifican a que URL realizar una redirección después de realizar Login o Logout.

LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"

Vamos a continuar creando las diferentes Plantillas (Templates) que necesitamos, una base, la home, y las de login y signup. Es bastante parecido a lo que ya vimos en el anterior Post.

Creamos una Plantilla base.html (templates/base.html) con el siguiente contenido. Incluye la lógica para mostrar u ocultar los enlaces de Login, Logout, y Signup, en función de si el usuario que visualiza la página está autenticado o no. Además, incluye una única zona o bloque de contenido, para que sea poblado por las Plantillas «hijas».

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Django Custom Users</title>
</head>
<body>

<header>
    {% if user.is_authenticated %}
        <p>Hi {{ user.username }}! <a href="{% url 'logout' %}">Logout</a></p>
    {% else %}
        <p>You are not logged in.
            <a href="{% url 'login' %}">Login</a> | <a href="{% url 'signup' %}">Signup</a></p>
    {% endif %}
</header>

<section>
    {% block content %}
    {% endblock %}
</section>

<footer>
    <p>&copy; 2023 - Django Custom Users. All rights reserved.</p>
</footer>
</body>
</html>

Creamos una Plantilla home.html (templates/home.html) con el siguiente contenido.

{% extends "base.html" %}

{% block content %}
<p>This is the Home page</p>
{% endblock %}

Creamos una Plantilla login.html (templates/registration/login.html) con el siguiente contenido. Incluye un formulario generado automáticamente por Django (form.as_p), así como la etiqueta csrf_token para protegernos de ataques CSRF (Cross-Site Request Forgery).

{% extends "base.html" %}

{% block content %}
<h1>Login</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock %}

Creamos una Plantilla signup.html (templates/registration/signup.html) con el siguiente contenido. Incluye un formulario generado automáticamente por Django (form.as_p), así la etiqueta csrf_token para protegernos de ataques CSRF (Cross-Site Request Forgery).

{% extends "base.html" %}

{% block content %}
<h1>Signup</h1>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Signup">
</form>
{% endblock %}

Ahora vamos a realizar la configuración de las URLs, de nuevo, bastante parecido a lo que ya hicimos en el anterior Post.

Editamos el fichero urls.py del Proyecto Django, y lo dejamos con el siguiente contenido. Aunque es bastante similar a lo que ya hicimos en el anterior Post, tiene de especial, que la página Home es servida directamente a través de la clase TemplateView, sin necesidad de crear una Aplicación Django para ella (las líneas resaltadas en negrita).

from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import TemplateView

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("accounts.urls")),
    path("accounts/", include("django.contrib.auth.urls")),
    path("", TemplateView.as_view(template_name="home.html"), name="home"),
]

Creamos un fichero urls.py en la Aplicación Django de accounts, con el siguiente contenido.

from django.urls import path
from .views import SignUpView

urlpatterns = [
    path("signup/", SignUpView.as_view(), name="signup"),
]

Ya sólo queda editar el fichero views.py de la Aplicación Django de accounts, y dejarlo con el siguiente contenido. Es similar a lo que hicimos en el anterior Post, con la diferencia de que en esta ocasión importamos y usamos el formulario CustomUserCreationForm, que creamos antes.

from .forms import CustomUserCreationForm
from django.urls import reverse_lazy
from django.views.generic import CreateView

class SignupView(CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy("login")
    template_name = "registration/signup.html"

Hecho esto ya habríamos acabado, y podríamos ejecutar nuestra aplicación y comprobar el resultado. Podemos registrar un usuario, y después logarnos con el. Un detalle importante, es que el formulario de Signup no incluye el cambio de email.

Sin embargo, si intentamos acceder con el usuario que acabamos de registrarnos (un usuario regular), al Panel de Administración de Django, nos aparecerá un mensaje invitándonos a logarnos de nuevo, ya que el usuario que estamos utilizando, no es un usuario Admin (el concepto Staff de Django).

Por otro lado, también podemos personalizar los campos que deseamos mostrar en los formularios, editando el fichero forms.py que creamos antes, por ejemplo, como se muestra a continuación.

from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser


class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm):
        model = CustomUser
        fields = (
            "username",
            "email",
            "department",
        )


class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = (
            "username",
            "email",
            "department",
        )

Si ahora probamos de nuevo, ahora si que aparece el campo email.

Formulario de Cambio de Contraseña

Sin hacer nada, podemos acceder al formulario de Cambio de Contraseña que proporciona Django por defecto, eso sí, con el aspecto del Panel de Administración de Django, pero totalmente funcional.

Si intentamos cambiar la contraseña, funcionará correctamente.

Django nos proporciona las Vistas y URLs de estas páginas, por lo que sólo tenemos que personalizar las Plantillas.

Crearemos una plantilla password_change_form.html (templates/registration/password_change_form.html) con el siguiente contenido, para personalizar la plantilla correspondiente al formulario de cambio de contraseña.

{% extends "base.html" %}

{% block content %}
<h1>Password change</h1>
<p>Please enter your old password, for security’s sake, and then enter your new password twice so we can verify you typed it in correctly.</p>
<form action="" method="post">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock %}

Del mismo modo, crearemos un fichero password_change_done.html (templates/registration/password_change_done.html) con el siguiente contenido, para personalizar la plantilla correspondiente a la página que muestra el mensaje de cambio de contraseña con éxito.

{% extends "base.html" %}

{% block content %}
<h1>Password change successful</h1>
<p>You password has changed with success.</p>
{% endblock %}

Y con esto ya estaría, a falta de añadir Bootstrap, Crispy Forms, y los CSS adicionales que podamos desear, el formulario ya está personalizado a nuestro gusto. Super sencillo.

Formulario de Reseteo de Contraseña

De forma similar a como ocurría con el cambio de contraseña, para el reseteo de contraseña Django también nos proporciona una implementación por defecto que podemos personalizar.

Este caso tiene la peculiaridad que necesitamos indicarle a Django cómo enviar emails, para lo que habitualmente utilizaremos un servicio externo basado en SMTP (ej: SendGrid, MailJet, MailChimp, etc), aunque en entornos de desarrollo y pruebas podemos utilizar otras alternativas (ej: Console backend, File backend, In-Memory backend, Dummy backend, etc).

En nuestro caso de ejemplo, vamos a configurar el Console Email Backend, para lo cual, tan sólo tendremos que añadir la siguiente línea en el fichero settings.py del proyecto Django.

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

Para probarlo, podemos acceder a la URL proporcionada por la implementación por defecto de Django, e intentar resetear la contraseña de un usuario.

Hecho esto, nos aparecerá la siguiente pantalla, informándonos que nos han enviado un email.

Como en nuestro caso estamos utilizando el Console Email Backend, deberemos revisar los mensajes de consola de nuestra aplicación. Allí encontraremos el output del email que envía la aplicación, donde podemos encontrar la URL de un único uso para restablecer la contraseña.

Utilizaremos dicha URL para restablecer nuestra contraseña.

Tras esto, habremos conseguido cambiar la contraseña, y podremos iniciar sesión con las nuevas credenciales.

Visto esto, ahora nos quedaría personalizar estas cuatro pantallas (plantillas). Empezaremos por el formulario para solicitar el reseteo de contraseña. Para ello crearemos la plantilla password_reset_form.html (templates/registration/password_reset_form.html) con el siguiente código.

{% extends "base.html" %}

{% block content %}
<h1>Forgot your password?</h1>
<p>Enter your email address below, and we'll email instructions for setting a new one.</p>
<form method="POST">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock %}

Continuaremos con la pantalla que informa el envío del correo con las instrucciones para el reseteo de contraseña. Para ello crearemos la plantilla password_reset_done.html (templates/registration/password_reset_done.html) con el siguiente código.

{% extends "base.html" %}

{% block content %}
<h1>Check your inbox.</h1>
<p>We've emailed you instructions for setting your password. You should receive the email shortly!</p>
{% endblock %}

Continuaremos con la pantalla de cambio de contraseña (la que responde a la URL de un sólo uso que recibimos por correo). Para ello crearemos la plantilla password_reset_confirm.html (templates/registration/password_reset_confirm.html) con el siguiente código.

{% extends "base.html" %}

{% block content %}
<h1>Set a new password!</h1>
<form method="POST">{% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Login">
</form>
{% endblock %}

Ya sólo nos queda la pantalla que nos informa del cambio de contraseña. Para ello crearemos la plantilla password_reset_complete.html (templates/registration/password_reset_complete.html) con el siguiente código.

{% extends "base.html" %}

{% block content %}
<h1>Password reset complete</h1>
<p>Your new password has been set.</p>
<p>You can log in now on the
<a href="{% url 'login' %}">Log In page</a>.</p>
{% endblock %}

El último detalle que quedaría, es añadir al formulario de Login, un enlace al formulario de reseteo de contraseña, como ayuda para los usuarios que no recuerden su contraseña y deseen resetearla, que la experiencia de usuario sea más sencilla y natural. Basta con añadir una línea.

Hecho esto, podemos probar de nuevo el reseteo de contraseña, en esta ocasión con las pantallas (plantillas) personalizadas, aunque a falta de aplicarles estilos, fuentes, Bootstrap, y otros detalles para tener un aspecto más chulo, pero completamente personalizadas y funcionales.

Configurar Django para enviar emails a través de SMTP

Si en lugar de utilizar el Console Email Backend, deseamos configurar el envío de emails a través de SMTP, deberemos configurar el fichero settings.py del proyecto Django de forma similar la siguiente ejemplo, en el que vemos cómo realizar la configuración para el envío de emails a través de SendGrid (sólo deberían cambiar las opciones DEFAULT_FROM_EMAIL y EMAIL_HOST_PASSWORD, en el caso de otro proveedor diferente a SendGrid pueden cambiar más opciones como EMAIL_HOST, etc).

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
DEFAULT_FROM_EMAIL = "your_email_account"
EMAIL_HOST = "smtp.sendgrid.net"
EMAIL_HOST_USER = "apikey"
EMAIL_HOST_PASSWORD = "sendgrid_password"
EMAIL_PORT = 587
EMAIL_USE_TLS = True

Personalizar los emails enviados por Django

Los emails enviado por Django, como el de reseteo de contraseña, consisten en una Plantilla, que podemos modificar igual que hemos hecho con las páginas. Por ejemplo, si buscamos en el GitHub de Django, la encontraremos como password_reset_email.html

Por lo tanto, podremos crearnos la plantilla en nuestro proyecto Django, y personalizarla a nuestro gusto, de forma similar a como acabamos de hacer con varias de las páginas (plantillas) que incluye Djando para el cambio o el reseteo de contraseña.

Cargar Configuraciones y Secretos desde Variables de Entorno

Es habitual que necesitemos utilizar Secretos, es decir, datos confidenciales como Contraseñas, API Keys, etc., que no debemos almacenar dentro de nuestro código fuente en Git, por el riesgo de que se puedan ver compremetidos.

También podemos necesitar Configuraciones, que sin ser confidenciales, tengan que cambiar según el entorno (ej: desarrollo, producción, etc.), quizás rutas donde almacenar ficheros, o cualquier otra cosa.

Una solución para esto, es el uso de Variables de Entorno, de tal modo que nuestra aplicación obtenga el valor de dichas variables y no tengamos que hardcodearlo en el código fuente con el problema adicional de que acabe en el historial de Git, algo que con Django nos permitirá personalizar las configuraciones del fichero settings.py.

Para este fin, podemos utilizar el paquete environs de Python, que facilita trabajar con variables de entorno y además permite utilizar ficheros de configuración «.env» para facilitar esta gestión, entre otras cosas. Para ello, añadiremos este paquete al fichero requirements.txt, y los instalaremos en nuestro Virtual Environment, para poder así tanto utilizarlo en PyCharm como que esté disponible al dockerizar nuestra aplicación.

Seguidamente, añadiremos las siguientes tres líneas al comienzo del fichero settings.py del Proyecto Django, para así poder utilizar la librería environs.

Creamos un fichero .env en la carpeta app, y añadimos en su interior la definición de las variables de entorno DEBUG y ALLOWED_HOSTS, de forma similar a como se muestra a continuación (ALLOWED_HOST es una lista, y permite uno o varios valores separados por comas).

Seguidamente, configuramos el fichero settings.py del Proyecto Django para que obtenga las configuraciones DEBUG y ALLOWED_HOSTS (esta última es obligatoria definirla cuando DEBUG=False) del fichero de configuración .env, pudiendo establecer un valor por defecto para el caso de que el fichero .env no defina dicha variable (por seguridad, por defecto será DEBUG=False y permitido el acceso sólo desde localhost).

Para probarlo, si intentamos acceder a una URL que no existe con DEBUG=True veremos que se revela información adicional que podría llegar a ser mal usada por un usuario malintencionado. Es decir, no sólo devuelve un 404, útil para depurar en nuestro entorno de desarrollo, pero un riesgo en Producción.

Sin embargo, con DEBUG=False (será necesario reiniciar la aplicación para aplicar el cambio de configuración, no sólo modificarlo en el fichero .env) se devolverá el error 404 sin mayor información.

También es muy habitual sacar a variable de entorno la configuración SECRET_KEY, por seguridad, y porque es recomendable utilizar valores diferentes en cada entorno (ej: dev, test, prod, etc). SECRET_KEY se utiliza para firmar y cifrar las sesiones, cifrar las contraseñas de los usuarios, firmar y verificar Tokens de autenticación, generar y validar tokens CSRF, etc., por lo que es recomendable mantenerla en secreto sin compartirla y usar valores diferentes para cada entorno.

Si lo deseamos, podemos utilizar Python para que nos genere valores seguros y aleatorios para SECRET_KEY, de forma similar a como se muestra en el siguiente ejemplo.

Hay muchas más formas de generar claves aleatorias, este sería otro ejemplo.

En nuestro caso, vamos a añadir una variable SECRET_KEY al fichero .env con el valor actual que tiene en el fichero settings.py, para seguidamente modificar el fichero settings.py para obtenga el valor del fichero .env, y así dejarlo listo para poder utilizar valores diferentes según el entorno y además que no se almacene en el historial de Git.

Otro caso necesario es la configuración del EMAIL_BACKEND, que igualmente podemos llevar a variables de entorno que leamos en el fichero settings.py de Django.

Otra configuración habitual, es la base de datos utilizada por la aplicación, que podemos definir en formato URL (DATABASE_URL), para facilitar esta configuración y llevarla al fichero .env o a variables de entorno, y leerla desde el fichero settings.py de Django. A continuación se muestra un ejemplo de esta configuración, utilizando SQLite3.

Para poder utilizar este formato de URL, es necesario instalar la librería dj-database-url, por lo tanto, la añadiremos al requirements.txt y la instalaremos.

Además, en caso de querer utilizar un motor de base de datos que no sea SQLite3, deberemos instalar la librería Python correspondiente. Por ejemplo, para poder utilizar PostgreSQL tendremos que añadir al requirements.txt el paquete psycopg2-binary e instalarlo, de forma similar a como se muestra en el siguiente ejemplo. Además tendremos que instalar el cliente de PostgreSQL (postgresql-client), a nivel de sistema operativo o de la imagen Docker.

Ejecución de Pruebas Unitarias (Unit Testing)

Vamos a hacer los Tests para la página de Signup, similar a los test realizados en Post anteriores (realmente del resto hemos sobrescrito las plantillas, es decir, código HTML y no Python):

  • Usamos TestCase al tratarse de código que accede a la base de datos.
  • Al realizar las llamadas con client.post, especificamos todos los campos que son usandos en el formulario.
  • Después de realizarse la acción, se realiza una redirección, por lo que comprobamos que se devuelve un código HTTP 302 (por ese motivo, no comprobamos un HTTP 200).
  • Necesitamos importar get_user_model().
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

class SignupPageTests(TestCase):
    def test_url_exists_at_correct_location_signupview(self):
        response = self.client.get("/accounts/signup/")
        self.assertEqual(response.status_code, 200)

    def test_url_available_by_name(self):
        response = self.client.get(reverse("signup"))
        self.assertEqual(response.status_code, 200)

    def test_template_name_correct(self):
        response = self.client.get(reverse("signup"))
        self.assertTemplateUsed(response, "registration/signup.html")

    def test_signup_form(self):
        response = self.client.post(
            reverse("signup"),
            {
                "username": "test_user",
                "email": "test_user@noemail.com",
                "department": "Technology",
                "password1": "Passw0rd33",
                "password2": "Passw0rd33",
            },
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(get_user_model().objects.all().count(), 1)
        self.assertEqual(get_user_model().objects.all()[0].username, "test_user")
        self.assertEqual(get_user_model().objects.all()[0].email, "test_user@noemail.com")

Ya sólo quedarían los últimos detalles, como ejecutar las pruebas unitarias y el sonar-scanner para subir los resultados del análisis de código y cobertura a Sonarqube, añadir el proyecto a un repo de Git, etc.

Despedida y Cierre

Hasta aquí llega este Post, en el que hemos visto paso a paso cómo extender el modelo de usuarios proporcionado por Django extendiendo la clase AbstractUser, para personalizarlo y adaptarlo a nuestras necesidades (añadir un nuevo atributo así como crear y personalizar los formularios de iniciar o cerrar sesión, registrarse, cambiar su password, o recuperarla), incluyendo detalles como el envío de emails y la gestión de la configuración desde variables de entorno o desde un fichero .env, todo esto a través de un ejemplo disponible en un repo público de GitHub: GitHub – ElWillieES – django-custom-users

Poco más por hoy. Como siempre, confío que la lectura resulte de interés.