How to Remove Username from Django User model

How to Remove Username from Django User model

Switching from username to another field such as email sounds like a simple task. But in Django, the User model is tighly coupled with other parts of the system.

In this post, I’ll show you how to mold the default User model to fit your needs.

User

The default user model is amazing because it comes with common functionality that almost every app will need.

Things like password hashing are taken care of.

On top of that, Django’s admin interface comes with a default ModelAdmin that makes CRUD against this model dead simple.

But what if you want to remove the username field entirely?

Subclassing AbstractUser

Django provides AbstractUser and AbstractBaseUser to allow you to create a custom User model

AbstractBaseUser allows you to built a User model from the ground up. Most of the boilerplate functionality available by default isn’t present in this class. This is great for projects that have completely weird authentication mechanisms.

In this post, we’ll conform to authentication where users don’t have usernames and instead rely on their email address to register and login.

In that case, we’ll subclass AbstractUser as follows:

from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as AuthUserManager
from django.db import models
from django.utils.translation import gettext_lazy as _


class User(AbstractUser):
  username = None
  email = models.EmailField(_("email address"), unique=True)

  USERNAME_FIELD = "email"
  REQUIRED_FIELDS = ()

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

  class Meta(AbstractUser.Meta):
    ordering = ["-date_joined"]
    db_table = "users"

Notice the following:

  1. We set username to None so that Django deletes the column from the database when we run migrations.
  2. Then, we add unique=True to the email field because by default, Django allows multiple users to have the same email address.
  3. After that, we set USERNAME_FIELD to be email because by default it’s username.
  4. Finally we set REQUIRED_FIELDS to an empty tuple because by default it’s set to email. If we don’t do that, Django will try to validate the presence of email twice which isn’t desirable.

At this point, you can simple use your new User model but the issue we have now is that neither Django’s admin nor Django’s commands like createuser are aware of this new model and can’t use it as is.

Change the AUTH_USER_MODEL setting to point to this new model, then generate migrations and apply them.

Once you have migrations ready, feel free to remove the username attribute from the class entirely.

Try creating a user at the command line and you’ll get errors.

To fix the problem, we’ll build a custom UserManager.

UserManager

Django uses a custom model manager called AuthUserManager to handle the creation of users.

We need to subclass it because by default, it looks for the username field which we deleted previously.

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
from django.utils.translation import gettext_lazy as _

from my_app.models import User


class UserManager(AuthUserManager):
  def _create_user(self, email, password, **extra_fields):
    email = self.normalize_email(email)

    user = self.model(email=email, **extra_fields)
    user.password = make_password(password)
    return user

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

  def create_superuser(self, email=None, password=None, **extra_fields):
    extra_fields.setdefault("is_staff", True)
    extra_fields.setdefault("is_superuser", True)

    if extra_fields.get("is_staff") is not True:
      raise ValueError("Superuser must have is_staff=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)

This is an almost 1:1 copy of the original UserManager but with every references to the default username field removed.

Now if you try to create a user use the createuser command, everything should work.

At this point, you can go ahead and start coding your actual app but I’m guessing you also use the Django admin to manage users. That’s why I’ll show you how to implement a custom UserAdmin next.

UserAdmin

Django comes with AuthUserAdmin that was built specifically for the default user model.

Let’s subclass it:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as AuthUserAdmin
from django.utils.translation import gettext_lazy as _


class UserAdmin(AuthUserAdmin):
  fieldsets = (
    (None, {"fields": ("email", "password")}),
    (
      _("Personal info"),
      {
        "fields": (
          "first_name",
          "last_name",
        )
      }
    ),
    (
      _("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"),
        }
    )
  )

  form = UserChangeForm
  add_form = UserCreationForm
  list_display = ("email", "first_name", "last_name", "is_staff")
  search_fields = ("first_name", "last_name", "email")
  ordering = ("email",)

admin.site.register(User, UserAdmin)

This is simply a copy of the original class but with all username stuff removed.

If you visit the admin interface now, you’ll find that you’re able to perform CRUD against your new custom User model.

Batteries included but removable

As you can see, Django makes it possible to do whatever you want. However, the core components are tightly coupled and care must be taken when changing any one part. That’s why it’s necessary to change managers and admins when you’re modifying built-in models.

That said, I’m satisfied with the way this is handled by Django. In this example, we switched to email for authentication but you can easily switch to alternative schemes like phone number, etc.

Feel free to email me if you think I made a mistake or to discuss this further.