Custom User Model no Django

Eu me enrolei, e sei que muita gente também se enrola quando se trata de User Authentication do framework. Mas agora seus problemas acabaram! ;)

Gabriel Gularte
7 min readJul 20, 2015

Qual o problema com o User Model default do Django?

A princípio, nenhum. Se você está começando no Django, só brincando, fazendo testes e experimentando não há nada de errado em usá-lo. O problema é quando você precisa desenvolver um grande projeto, sério, cisudo e pomposo e vai colocá-lo em produção.

O User default atende muito bem quase todas expectativas de um projeto do mundo real. Mas ele acaba atrapalhando, e até desencorajando os novos djanglers na hora da customização. Isso porque esse model não permite muita customização, e a recomendação na documentação do próprio Django é que você extenda esse model ou crie o seu próprio. É nessa hora que a coisa começa a ficar complicada…

Extendendo o User Model

A maneira mais simples de adicionar alguma customização pra esse model, é criando um outro model com um relacionamento OneToOne pra ele. Para exemplificar, imagine que você planejou que seus usuários tenham perfis, e seus perfis têm dados como: idade e gênero. Extendendo o User default com um relacionamento OneToOne, seu código ficaria mais ou menos assim:

from django.contrib.auth.models import User
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(User, related_name='profile')
age = models.IntegerField()
gender = models.BooleanField()

Ps: Todos os blocos de código desse post vão estar meio sem identação. Eu não consegui fazer a identação direitinho neles… Se alguém souber como, ajuda bastante! ;)

Dessa forma você extendeu o User default do Django e criou uma nova entidade para comportar os campos que ele não oferece nativamente.

Substituindo o User Model

Uma vez que você não precisa alterar o comportamento nativo do User default, a solução anterior se aplica perfeitamente a você. Mas e se, por exemplo, o usuário do nosso site precisa se logar com o email ao invés de username? Ou se a senha do usuário necessita ter um limite de 8 caracteres? Aqui está a triste verdade: não existe uma maneira fácil e/ou painless de alterar esses comportamentos. Você precisa meter a mão na massa e ralar um pouquinho…

Mas como eu já ralei bastante, vou tentar diminuir seu sofrimento, ok? Vamos lá, como eu disse, você precisa que o seu usuário se logue no nosso site com o email, e não com o seu username. Além disso, você quer uma informação importante sobre o usuário, quer saber se ele confirmou seu email, ou seja, se ele realmente existe e é confiável. Para isso nós vamos substituir o User Model default do Django e usar nosso próprio model.

É muito importante ressaltar aqui que você inicie um novo projeto para esse método. Se você tentar substituir o User Model com uma aplicação viva ou um projeto que já tenha qualquer tipo de estrutura, pode dar merda, e você pode acabar num beco sem saída, ok?

Pra começar, vamos criar um novo app chamado account:

python manage.py startapp account

Agora a brincadeira começa. Vamos deixar claro que a repetição vai existir aonde você não quer alterar, ou seja, nós vamos repetir aqui o comportamento de alguns campos, portanto, se você só precisa alterar um comportamento, tenha certeza de ser extremamente necessário, ok? Vamos começar a escrever nosso próprio User model:

# Arquivo models.py do app accountfrom django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.core import validators
from django.utils.translation import ugettext_lazy as _
import re
class User(AbstractBaseUser, PermissionsMixin):

username = models.CharField(_('username'), max_length=15, unique=True, help_text=_('Required. 15 characters or fewer. Letters, numbers and @/./+/-/_ characters'), validators=[ validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), _('invalid'))])
first_name = models.CharField(_('first name'), max_length=30) last_name = models.CharField(_('last name'), max_length=30) email = models.EmailField(_('email address'), max_length=255, unique=True) is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.')) is_active = models.BooleanField(_('active'), default=True, help_text=_('Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'))

date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

Até aí, estamos bem perto do model default, agora vamos adicionar um campo a mais nesse model:

is_trusty = models.BooleanField(_('trusty'), default=False, help_text=_('Designates whether this user has confirmed his account.'))

Esse bloco de código acrescenta o campo is_trusty, que nos dirá se o usuário confirmou o email, ou seja, se é um usuário confiável. A estrutura do nosso model está basicamente pronta, vamos agora trocar a forma de login do usuário, que será via email. Vamos também adicionar os campos obrigatórios e uns métodos de apoio. Ainda na nossa classe User:

USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
return self.first_name
def email_user(self, subject, message, from_email=None):
send_mail(subject, message, from_email, [self.email])

Agora sim nós temos nosso model (quase) completo. Ainda vai ficar faltando uma linhazinha, mas a gente já fala dela. Vamos analisar o código acima:

  • USERNAME_FIELD: essa variável recebe o campo do model que será utilizado pelo Django para autenticação. É aqui que ao invés de username do model default, eu coloco email para fazer o login pelo email do usuário.
  • REQUIRED_FIELDS: os campos que são obrigatórios desse model. Nenhuma dúvida quanto isso, certo?
  • class Meta, verbose_name e verbose_plural: aqui a gente define a taxonomia que nosso model terá para o Django no singular e no plural.
  • get_full_name, get_short_name e email_user: são métodos de apoio que facilitam nossa vida. Se eu não me engano, elas são obrigatórias para o sistema do Django.

Agora nós precisamos criar um manager pro nosso model. O manager é a classe que disponibiliza funções para criação e administração do usuário e superuser. Vamos escrever nosso manager, no mesmo arquivo, porém em outra classe e acima da nossa classe User (já explico o motivo):

class UserManager(BaseUserManager):    def _create_user(self, username, email, password, is_staff, is_superuser, **extra_fields):
now = timezone.now()
if not username:
raise ValueError(_(‘The given username must be set’))
email = self.normalize_email(email)
user = self.model(username=username, email=email, is_staff=is_staff, is_active=True, is_superuser=is_superuser, last_login=now, date_joined=now, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
return self._create_user(username, email, password, False, False, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
user=self._create_user(username, email, password, True, True, **extra_fields)
user.is_active=True
user.save(using=self._db)
return user

Depois de criarmos o User Manager, voltamos ao User para inserir o Manager. E é aqui que entendemos o motivo de inserir a classe UserManager acima da User. A classe User faz referência a UserManager, ou seja, se colocarmos a UserManager depois vamos causar um erro, pois a aplicação não vai econtrar UserManager quando for parsear essa linha.

...
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name', 'last_name']
objects = UserManager()class Meta:
...

O arquivo final, deve se parecer com o código abaixo, devidamente identado, claro…

import refrom django.db import models
from django.core import validators
from django.utils import timezone
from django.core.mail import send_mail
from django.utils.http import urlquote
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.conf import settings
class UserManager(BaseUserManager):def _create_user(self, username, email, password, is_staff, is_superuser, **extra_fields):
now = timezone.now()
if not username:
raise ValueError(_(‘The given username must be set’))
email = self.normalize_email(email)
user = self.model(username=username, email=email,
is_staff=is_staff, is_active=True,
is_superuser=is_superuser, last_login=now,
date_joined=now, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email=None, password=None, **extra_fields):
return self._create_user(username, email, password, False, False,
**extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
user=self._create_user(username, email, password, True, True,
**extra_fields)
user.is_active=True
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):username = models.CharField(_(‘username’), max_length=15, unique=True,
help_text=_(‘Required. 15 characters or fewer. Letters, \
numbers and @/./+/-/_ characters’),
validators=[
validators.RegexValidator(
re.compile(‘^[\w.@+-]+$’),
_(‘Enter a valid username.’),
_(‘invalid’))])
first_name = models.CharField(_(‘first name’), max_length=30)
last_name = models.CharField(_(‘last name’), max_length=30)
email = models.EmailField(_(‘email address’), max_length=255, unique=True)
is_staff = models.BooleanField(_(‘staff status’), default=False,
help_text=_(‘Designates whether the user can log into this admin site.’))
is_active = models.BooleanField(_(‘active’), default=True,
help_text=_(‘Designates whether this user should be treated as active. \
Unselect this instead of deleting accounts.’))
date_joined = models.DateTimeField(_(‘date joined’), default=timezone.now)
is_trusty = models.BooleanField(_(‘trusty’), default=False,
help_text=_(‘Designates whether this user has confirmed his account.’))
USERNAME_FIELD = ‘email’
REQUIRED_FIELDS = [‘username’, ‘first_name’, ‘last_name’]
objects = UserManager()class Meta:
verbose_name = _(‘user’)
verbose_name_plural = _(‘users’)
def get_full_name(self):
full_name = ‘%s %s’ % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
return self.first_name
def email_user(self, subject, message, from_email=None):
send_mail(subject, message, from_email, [self.email])

Feito isso, agora precisamos dizer ao Django que queremos usar o nosso model ao invés do default dele. Pra isso, vamos adicionar uma linha ao arquivo settings.py:

...
ALLOWED_HOSTS = []
# Eu coloquei aqui entre o ALLOWED_HOSTS e o LOGIN_URL, mas você
# pode colocar aonde quiser, desde que faça algum sentido.
AUTH_USER_MODEL = ‘account.User’
LOGIN_URL = ‘/’
...

Não se esqueça de adicionar o app “account” no INSTALLED_APPS!

Agora basta rodar um makemigrations e migrate da account, criar um superuser e voilà!

python manage.py makemigrations account
python manage.py migrate account
# Eu gosto de rodar um migrate puro por segurança...
python manage.py migrate
python manage.py createsuperuser
Login no admin do Django com email ao invés de username (sim, estou num Windows… Não me julgue! Rsrs)

Espero que não tenha sido muito maçante ler esse artigo imenso! Haha. O código desse artigo está no Github, e já vem até com uma Vagrant se você quiser levantar uma VM de brinks pra testar. É isso galera, aquele abraço!

--

--