django-custom-user-min

Django ships with a built-in User model for authentication, however, the official Django documentation highly recommends using a custom user model for new projects.

There are two modern ways to create a custom user model in Django: AbstractUser and AbstractBaseUser. In both cases, we can subclass them to extend existing functionality.

However, AbstractBaseUser requires much, much more work. So here we will use AbstractUser. 

We will also create a UserProfile model to store additional information about users with the help of a One-To-One Link. After this, we will extend the BaseUserManager functionality a little bit. We will also configure signals to automatically create UserProfile whenever a User object is created. Finally, we will customize the Django admin.

Note: This should be done at the begining of the project before running any migrations.

Step 1: New app

  • Create app users using the command python manage.py startapp users
  • Now register your app in your installed apps in settings.py: INSTALLED_APPS += [ 'users', ]
  • Set¬†AUTH_USER_MODEL¬†in settings.py to the new user model:AUTH_USER_MODEL = 'users.User'

Step 2: Extend model and manager

Create a file constants.py inside users app directory

# users/constants.py
SUPERUSER = 1
STAFF = 2
STUDENT = 3
TUTOR = 4

USER_TYPE_CHOICES = (
      (SUPERUSER, 'superuser'),
      (STAFF, 'staff'),
      (STUDENT, 'student'),
      (TUTOR, 'tutor'),
  )

Extend the User model in your users/models.py

# users/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
from apps.users.managers import UserManager
from . import constants as user_constants

class User(AbstractUser):
    username = None # remove username field, we will use email as unique identifier
    email = models.EmailField(unique=True, null=True, db_index=True)
    is_active = models.BooleanField(default=False)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)
    user_type = models.PositiveSmallIntegerField(choices=user_constants.USER_TYPE_CHOICES)

    REQUIRED_FIELDS = []
    USERNAME_FIELD = 'email'

    objects = UserManager()


class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True,related_name="user_profile")
    phone = models.CharField(max_length=255,blank=True,null=True)
    is_verified = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def __str__(self):
        return self.user.email

Extend UserManager in users/managers.py

# users/managers.py

from django.contrib.auth.models import BaseUserManager
from . import constants as user_constants


class UserManager(BaseUserManager):
    def create_user(self, email, password, **extra_fields):

        if not email:
            raise ValueError('The Email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_active', True)
        extra_fields.setdefault('user_type', user_constants.SUPERUSER)
        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)

Step 3: Migrations

Run the commands below to make database changes

python manage.py makemigrations 
python manage.py migrate

Step 4: Create Signal

Now let's create signals for automatically creating a UserProfile instance in users/signals.py

# users/signals.py
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import UserProfile, User

@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)
    instance.user_profile.save()

Now, register your signals file inside users/apps.py by adding a method ready and importing the signals file path.

# users/apps.py
from django.apps import AppConfig

class UsersConfig(AppConfig):
    name = 'apps.users'

    def ready(self):
        import apps.users.signals

Now, add default_app_config to users/init.py

# users/__init__.py
default_app_config = 'users.apps.UsersConfig'

Step 5: Extend the admin form

Let's extend UserCreationForm  and UserChangeForm in users/forms.py

# users/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import User


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm):
        model = User
        fields = ('email',)


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = User
        fields = ('email',)

Step 6: Customize Django user admin

Let's customize Django's admin now

# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import UserProfile, User
from .forms import CustomUserCreationForm, CustomUserChangeForm


class UserProfileInline(admin.StackedInline):
    model = UserProfile
    can_delete=False
    verbose_plural_name="User Profile"
    fk_name = 'user'  

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = User
    list_display_links = ['email']
    search_fields = ('email',)
    ordering = ('email',)
    # inlines = (UserProfileInline,)
    list_display = ('email', 'is_staff', 'is_active', 'is_superuser',)
    list_filter = ('email', 'is_staff', 'is_active', 'is_superuser', 'user_type')
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email','user_type')}),
        (_('Permissions'), {
            'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'),
        }),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'password1', 'password2', 'is_staff', 'is_active', 'user_type')}
         ),
    )

    def get_inline_instances(self, request, obj=None):
        if not obj:
            return list()
        return super(CustomUserAdmin, self).get_inline_instances(request, obj)

admin.site.register(User, CustomUserAdmin) 

Step 7: Verifying

You have successfully extended the user model. Let's create a user

python manage.py createsuperuser

#output
Email: [email protected]
Password: ..........
Password (again): ..........
Superuser created successfully

You user should be created by now, login into your admin panel to see.

That's all. If you like the tutorial, do subscribe to this python newsletter for more Django updates.