Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 34 de 45

Modelo de usuario personalizado

La decisión que hay que tomar en el primer día del proyecto, el error exacto que sale si te acuerdas tarde, y las tres formas de hacerlo.

⏱ 15 min de lectura

El modelo User que has estado usando trae unos supuestos que conviene mirar de cerca, porque cambiarlos después es caro. Medidos:

Salida en pantalla
username    CharField   max_length=150  unique=True   blank=False
email       EmailField  max_length=254  unique=False  blank=True
first_name  CharField   max_length=150                blank=True
last_name   CharField   max_length=150                blank=True

El correo no es único y puede estar vacío. Comprobado:

Salida en pantalla
dos usuarios con el mismo correo: 5, 6

Para casi cualquier sitio actual eso está mal, porque la gente entra con su correo y espera recuperar la contraseña con él. Y a eso se suman un nombre de usuario obligatorio que nadie quiere inventarse y un nombre partido en dos que no funciona en medio mundo.

Todo se arregla con un modelo propio. El problema es cuándo.

Primero, la advertencia#

Un proyecto ya migrado con auth.User, al que se le cambia el modelo después:

Salida en pantalla
django.db.migrations.exceptions.InconsistentMigrationHistory:
Migration admin.0001_initial is applied before its dependency
cuentas.0001_initial on database 'default'.

Eso es lo que sale, y no hay ningún ajuste que lo arregle. El motivo es que medio Django apunta al modelo de usuario: el panel, las sesiones, los permisos, los grupos y el historial. Cambiarlo significa reescribir todas esas claves ajenas en tablas que ya tienen datos.

Las salidas son dos, y ninguna es agradable. En desarrollo, borrar la base de datos y las migraciones y empezar de nuevo, que es lo que se hace en la práctica. En producción, una secuencia de migraciones manuales que la documentación de Django describe con la palabra "difícil".

Las tres estrategias#

EstrategiaCuándoCoste
Modelo aparte con OneToOneEl proyecto ya está en producción con auth.UserUna consulta más y un objeto que puede no existir
AbstractUserCasi siempreNinguno si se hace al principio
AbstractBaseUserAutenticación de verdad distintaHay que escribir bastante

El perfil aparte#

python
class Perfil(models.Model):
        usuario = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    telefono = models.CharField(max_length=20, blank=True)

Es lo que se hacía antes de que existieran las otras dos y es la única opción cuando ya es tarde. Funciona y tiene dos pegas que la lección 14 ya midió: cada acceso al perfil es otra consulta si no se pide con select_related, y la vuelta de un OneToOne lanza una excepción en vez de devolver None cuando no existe, lo que obliga a crear el perfil en el registro y a acordarse de los usuarios antiguos.

AbstractUser#

La que se usa el noventa por ciento de las veces. Hereda todo lo que tenía User y deja añadir y redefinir:

cuentas/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models


class Usuario(AbstractUser):
        email = models.EmailField('correo', unique=True)
    telefono = models.CharField('teléfono', max_length=20, blank=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['username']

    def __str__(self):
                return self.email
sitio/settings.py
AUTH_USER_MODEL = 'cuentas.Usuario'

Esa cadena es aplicacion.Modelo, no la ruta de importación.

Los dos atributos de en medio son los que cambian cómo se entra:

USERNAME_FIELD es el campo que identifica. Al ponerlo en email, se entra con el correo. Tiene que ser único, y por eso la línea de arriba lo redefine.

REQUIRED_FIELDS es lo que pide createsuperuser además del identificador y la contraseña. No se pone ahí ni USERNAME_FIELD ni la contraseña.

Medido:

Salida en pantalla
USERNAME_FIELD: email | REQUIRED_FIELDS: ['username']
autenticar con el correo:  jefe@ejemplo.es
autenticar con el nombre:  None

El nombre de usuario ya no sirve para entrar, aunque siga existiendo. Si no lo quieres, se quita con username = None y REQUIRED_FIELDS = [], y entonces hace falta un gestor propio porque create_user espera un nombre.

AbstractBaseUser#

Solo trae la contraseña y la marca de última entrada. Todo lo demás —los permisos, las banderas, el gestor— hay que escribirlo o heredarlo de PermissionsMixin.

Es lo que hace falta cuando la autenticación no se parece a un usuario con contraseña: entrar solo con el teléfono y un código, identificarse contra un sistema externo, o no tener contraseña en absoluto.

Si dudas entre esta y AbstractUser, es AbstractUser.

El panel#

El modelo propio hay que registrarlo, y no con un ModelAdmin normal:

cuentas/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import Usuario


@admin.register(Usuario)
class UsuarioAdmin(UserAdmin):
        list_display = ['email', 'username', 'is_staff']
    ordering = ['email']
    fieldsets = UserAdmin.fieldsets + ((None, {'fields': ['telefono']}),)
Salida en pantalla
login en el admin: True
/admin/cuentas/usuario/          ->  200
/admin/cuentas/usuario/1/change/ ->  200

Heredar de UserAdmin y no de ModelAdmin es lo que trae el formulario de contraseña que cifra en lugar de guardar el texto, el enlace para cambiarla y la sección de permisos.

Y fíjate en el fieldsets = UserAdmin.fieldsets + (...): hay que sumar a los del padre, no sustituirlos, o los campos heredados desaparecen del formulario. Es el mismo razonamiento del class Meta(UserCreationForm.Meta) de la lección 32.

El ordering tampoco es decorativo: el de UserAdmin ordena por username, y si has quitado ese campo, el panel falla.