Cómo extender el User de Django

El sistema de usuarios y autenticación integrado en Django es muy completo,  lo podemos adaptar a la mayoria de los casos de uso, pero hay situaciones en las que no podemos utilizarlo “out-of-the-box”. Por ejemplo, si nuestro proyecto tuviese carácter social es probable que necesitásemos almacenar una pequeña biografía, la localización del usuario y alguna otra información de interés. En esta entrada veremos algunas estrategias para extender el User Model que nos proporciona Django según nos convenga.

En términos general, hay cuatro estrategias diferentes para extender el modelo de usuarios existente, a continuación veremos cuándo y cómo usar cada una de ellas.

Modelo Proxy

Un Proxy Model no es más que un modelo de herencia en el cual no se modifica el esquema de la base de datos. Se utiliza para cambiar el comportamiento de un modelo existente.

Usaremos un Proxy Model cuando no necesitemos almacenar información extra en la base de datos, sino simplemente necesitemos añadir alguna funcionalidad extra, como por ejemplo cambiar el query manager. Es la forma menos intrusiva de extender el modelo de usuarios, pero también es la más limitada. Veamos un ejemplo:

from django.contrib.auth.models import User
from .managers import PersonManager

class MyUser(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        pass

Hemos definido un Proxy Model llamado MyUser, para ello hemos añadido dentro de la metaclase la propiedad proxy = True. En este caso hemos redefinido el orden predeterminado, asignando un Manager personalizado y un nuevo método do_something.

Cabe resaltar que User.objects.all() y MyUser.objects.all() generarán la misma consulta a la base de datos, la única diferencia es el comportamiento definido en el Proxy Model.

Modelo Profile

Es un modelo estándar de Django el cual tendrá su propia tabla en la base de datos y una relación uno a uno con el User Model existente, a través de un OneToOneField. Utilizaremos esta aproximación cuando necesitamos almacenar información adicional que no esté relacionada con el proceso de autenticación.

Es muy probable que esta opción sea la que mejor se adapta a la mayoría de los casos, personalmente es la que más uso. Básicamente lo que haremos será crear un nuevo modelo de Django en el cual se almacenará toda la información extra. El uso de esta estrategia da como resultado una consulta extra a la base de datos, es importante tenerlo en cuenta para no caer en el clásico problema del N+1 el cual comentamos en este post.

Implementación del modelo Profile:

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

A continuación definiremos las “signals” (señales) que nos ayudarán a crear/actualizar “automágicamente” nuestro Profile.

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

En el código anterior lo único hacemos es subscribir los métodos create_user_profile y save_user_profile al evento “save” del modelo User. Este tipo de señal se denomina post_save.

Así quedaría la plantilla de nuestro profile:

<h2>{{ user.get_full_name }}</h2>
<ul>
<li>Username: {{ user.username }}</li>
<li>Location: {{ user.profile.location }}</li>
<li>Birth Date: {{ user.profile.birth_date }}</li>
</ul>

¿Qué tendríamos dentro del la vista?

def update_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
    user.save()

¿Y si usaramos Django Forms?
Veamos como procesar más de un formulario a la vez, echemos un vistazo al siguiente snippet:
forms.py

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('url', 'location', 'company')

views.py

class ProfileVIew(View):
    def get(self, request):
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
        return render(request, 'profiles/profile.html', {
            'user_form': user_form,
            'profile_form': profile_form
        })
        
    def post(self, request):
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Your profile was successfully updated!'))
            return redirect('settings:profile')
    
        messages.error(request, _('Please correct the error below.'))

profile.html

<br />
<form method="post">
  {% csrf_token %}
  {{ user_form.as_p }}
  {{ profile_form.as_p }}
  <button type="submit">Save changes</button>
</form>
<p>

Heredar de AbstractBaseUser

En este caso, crearemos un nuevo modelo a partir de la clase AbstractBaseUser, en este caso es importante valorar si realmente lo necesitamos ya que el coste en tiempo de desarrollo aumenta. Esta estrategia se suele utilizar cuando necesitamos de requisitos especificos de autenticación, como podría ser utilizar el email como identificador de acceso.

Implementación del modelo:

from __future__ import unicode_literals

from django.db import models
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        '''
        Returns the first_name plus the last_name, with a space in between.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        '''
        Returns the short name for the user.
        '''
        return self.first_name

En nuestro modelo, además de añadir nuestros campos personalizados, debemos implementar obligatoriamente los métodos get_full_name() y get_short_name(). Además hay que indicar cual va a ser el campo identificador del usuario (USERNAME_FIELD) y cuales van a ser los campos requeridos (REQUIRED_FIELDS).
Otra pieza fundamental del puzzle es la clase UserManager, debemos referenciarla en la propiedad objects, se encargará de manejar el “query set” del User Model. Aquí  implementaremos, cómo mínimo, los métodos create_user y create_superuser:

from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)

Por último, nos faltaría indicar en settings.py nuestro nuevo User Model el cual utilizaremos para autenticarnos:

AUTH_USER_MODEL = 'accounts.User'

Podemos referenciar nuestro User Model de dos maneras:
1. Importando nuestra app:

from django.db import models
from testapp.core.models import User

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(User, on_delete=models.CASCADE)

2. Referenciando desde settings.py:

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

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)

Esta última manera nos permite desacoplar en cierta manera la nuestra de usuarios app.

Heredar de AbstractUser

En este caso crearemos un nuevo modelo heredando de AbstractUser. Esta aproximación la utilizaremos cuando necesitemos añadir algún campo al modelo existente y no se requiera de cambios en el sistema de autenticación.

En el siguiente snippet veremos lo sencillo que resulta implementar esta estrategia:

from django.db import models

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

Al igual que en el caso anterior deberemos indicar en settings.py cual es nuestro User Model:

from django.db import models
AUTH_USER_MODEL = 'myAccount.User'

Conclusiones

En resumen, se han descrito cuatro estrategias diferentes para extender el modelo de usuarios de Django, como hemos podido observar no hay ninguna solución definitiva, la elección de una u otra estrategia dependerá del nivel de personalización y de los requisitos de autenticación de nuestro proyecto:

  • Modelo Proxy: El sistema de autenticación y el modelo de usuarios cumple, pero necesitamos añadir o modificar alguna funcionalidad.
  • User Profile: El sistema de autenticación cumple con los requisitos pero queremos añadir algún campo personalizado a nuestro modelo.
  • Heredar de AbstractBaseUser: La autenticación de Django no se ajusta a tu proyecto.
  • Heredar de AbstractUser: La autenticación cumple, pero necesitas añadir campos al usuario sin crear un nuevo modelo.

 

Cómo extender el User de Django
5 (100%) 9 votos
15 comentarios
  1. BernardoGarcíaLoaiza (@bgarcial)
    BernardoGarcíaLoaiza (@bgarcial) Dice:

    Muy chévere, gran entrada y muy ilustrativa para entender el proceso de flujo de trabajo (al menos básico) del modelo de usuarios de Django, personalmente he usado las alternativas de heredar de AbstractUser, AbstractBaseUser y el del modelo Profile.

    En estos momentos estoy usando en un proyecto AbstractBaseUser dado que requería cambiar la clave primaria de mi tabla User, que no fuera el id, sino el username como clave primaria, para lo cual heredar de AbstractUser no me lo permitía, pues tengo entendido que se debe usar solo cuando queremos adicionar campos al modelo de usuarios original, pero no para modificar cosas como su clave primaria.
    Acá hablan un poquito de ello http://stackoverflow.com/questions/21514354/difference-between-abstractuser-and-abstractbaseuser-in-django

    No se si era necesario AbstractBaseUser para esto o no, pensaría que si, pues con AbstractUser lo que me sucedia era que rompía la funcionalidad de autenticación, pese a que en mi modelo no la ajusto mucho.

    No se si tengas algunas consideraciones diferentes al respecto, de las cuales me interesaría leerlas.

    Actualmente estoy mirando como trabajar roles o permisos para usuarios en base a diferentes tipos, me gustaría (y perdona el abuso de confianza) que tuvieras algo al respecto de orientación, más allá de uno ir a los docs oficiales.

    Me gusta tu blog. Felicidades.

    Responder
    • miguelghz
      miguelghz Dice:

      Hola Bernardo, gracias por el comentario, me alegra que te guste el blog, aún me queda mucho camino por recorrer.
      En cuanto a tu problema, has hecho bien heredando de AbstractBaseUser para customizar el User, ya que la opción de extender AbstractUser es más limitada. Sobre el tema de roles y permisos tienes que hacer que tu clase User también herede de PermissionsMixin el cual se encuentra en el modulo django.contrib.auth.models, además probablemente tendrás que crear tu propio “UserAuthBackend”. Cómo alternativa, tienes apps de la comunidad para autenticación como Django Guardian o Django Rules.

      Espero haberte ayudado! Saludos!

      Responder
  2. Fernando V
    Fernando V Dice:

    Hola Miguel Muy bueno el articulo! Gracias por el material.
    Estaba intentando implementar el Modelo Profile, pero resulta que los usuarios que tengo creados dan problema que estos usuarios no tienen Profile, lo cual es cierto no tienen, pero ahora no puedo entrar al Admin con el superuser por que tambien se vio afectado por la modificacion.
    Cual puede ser el problema o una solucion?

    Responder
  3. Ernesto
    Ernesto Dice:

    Muy buen post. Me sirvió de mucho y aclaro varias dudas que tenia con respecto a la herencia. Tengo un problema que no se como solucionarlo y la temática se relaciona con el tema de este post. El problema es que he desarrollado una projecto modular, en la cual algunos de los módulos(apps) se pueden instalar y desisntalar, lo cual se traduce en crear o eliminar sus respectivas tablas en la db a traves de migraciones. Todo me funcionaba genial hasta que decidí heredar una clase del modulo(app) A en el modulo(app) B. En este caso si el modulo B esta instalado y quiero eliminar una instancia del modelo padre en el modulo A no hay problema, pero si el modulo B no esta instalado y trato de eliminar el mismo objeto del modelo padre del modulo A, me da un error parecido a este: “no existe la relación «report_engine_gkguest….”, donde “report_engine” es el modulo B y “gkguest” el modelo hijo que hereda del modelo “Guest” que esta en otro modulo(app). Supongo que el problema en cuestión pueda solucionarse eliminando la herencia e introduciendo una relación OneToOne, pero quisiera saber si hay alguna vía de manteniendo la herencia solucionar este problema? Gracias por su tiempo

    Responder

Dejar un comentario

¿Quieres unirte a la conversación?
Siéntete libre de contribuir

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *