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.
Gracias Miguel. Bien escrito!
Gracias a ti por el feedback! Saludos!
muy buena explicacion, esperamos mas articulos asi
Gracias Edgar, esa es la intención. Saludos!
Excelente
Gracias!
Muchas gracias por la información y la capacitación.
Gracias por el comentario!
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.
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!
Miguel, muchas gracias por tu respuesta.
De pronto en que contexto esto de crear mi propio ¿”UserAuthBackend”? ¿Es para lo de los roles?
De otro lado, antes uno creaba un propio backend de autenticación para poner a que en mi esuqema de autenticación se iniciara sesión con el email y no con el username.
Ahora con lo que se hizo en managers.py y con poner el atributo email en USERNAME_FIELD es suficiente ¿verdad?
Todavia, actualmente prefiero reescribirme un backend de autenticacion que sobreescribir el user para poder loguearme con un email.
https://docs.djangoproject.com/es/1.10/topics/auth/customizing/#authentication-backends
Victor, ¿por qué prefieres realizar un backend de autenticación? ¿Que diferencia hay lo uno con lo otro?
Estupendo material !
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?
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
trate de ejecutar el primer codigo que se refiere al Model Proxy, sin embargo Django me arroja este error:
from .managers import PersonManager
ImportError: No module named ‘apps.proxy.managers’
Hola Iván, el código de los ejemplos no está completo. En el caso del Model Proxy, el “PersonManager” hace referencia a un supuesto conjunto de querysets personalizado.
Para que te funcione simplemente elimina la línea “objects = PersonManager()” y crea los métodos que necesites para definir el comportamiento del modelo.
Saludos.
Muchas gracias Miguel, efectivamente es como indicas.
Hola Miguel,
Primero que todo muchas gracias por la info, esta excelente. Está mucho mejor explicada que en el mismo djangoproject. Queria aprovechar de preguntarte que me recomiendas para definir 2 tipos de usuarios, en mi modelo de base de datos (aun conceptual) tengo una tabla persona con todo lo común, otra proveedor con ciertas cosas particulares y otra consumidor que tiene a los proveedores preferidos. Como crees que pueda ser la manera correcta de definir lo anterior.
Muchas gracias por el blog, se va directamente a mis favoritos
hola, muchas gracias por el post me ha sido de mucha utilidad, respecto al link de n+1:
http://miguelgomez.io/optimizando-django-rest-framework/
esta te manda de vuelta al home, dejo aqui el link del post en lo que arreglas este error
https://miguelgomez.io/django/optimizando-django-rest-framework/
Hola, me gusto mucho el contenido, pero al querer realizarlo en mi proyecto, me da el siguiente error:
ValueError: The field admin.LogEntry.user was declared with a lazy reference to ‘Admin.myuser’, but app ‘Admin’ doesn’t provide model ‘myuser’.
Me gustaría mucho su ayuda