Caktus groups How to Switch to a Custom Django User Model mid-project

TL;DR

  • create a users application with a custom User

  • execute 2 SQL commands:

    INSERT INTO django_migrations (app, name, applied)
    VALUES ('users', '0001_initial', CURRENT_TIMESTAMP);
    

    and

    UPDATE django_content_type SET app_label = 'users'
    WHERE app_label = 'auth' and model = 'user';
    

1) Start a new users app

Start a new users app (or give it another name of your choice, such as accounts ).

If preferred, you can use an existing app, but it must be an app without any pre-existing migration history because as noted in the Django documentation, “due to limitations of Django’s dynamic dependency feature for swappable models, the model referenced by AUTH_USER_MODEL must be created in the first migration of its app (usually called 0001_initial); otherwise, you’ll have dependency issues.”

python manage.py startapp users

2) Add a new User model to users/models.py

Add a new User model to users/models.py, with a db_table that will make it use the same database table as the existing auth.User model.

For simplicity when updating content types later (and if you’d like your many-to-many table naming in the underlying database schema to match the name of your user model), you should call it User as I’ve done here. You can rename it later if you like:

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


class User(AbstractUser):
        class Meta:
            db_table = 'auth_user'

Warning

do not forget db_table = “auth_user”

3) users admin

As a convenience, if you’d like to inspect the user model via the admin as you go, add an entry for it to users/admin.py:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User


admin.site.register(User, UserAdmin)

4) update settings.py

In settings.py, add users to INSTALLED_APPS and set AUTH_USER_MODEL = ‘users.User’:

INSTALLED_APPS = [
        # ...
        'users',
]

AUTH_USER_MODEL = 'users.User'

5) Migration

makemigrations

Create an initial migration for your new User model:

python manage.py makemigrations users

You should end up with a new migration file users/migrations/0001_initial.py.

showmigrations

We can see the migration with the showmigrations django extension command.

python manage.py showmigrations
users
 [ ] 0001_initial

6) SQL commands for fake migration

Since the auth_user table already exists , normally in this situation we would fake this migration with the command: python manage.py migrate users –fake-initial.

If you try to run that now, however, you’ll get an InconsistentMigrationHistory error, because Django performs a sanity check before faking the migration that prevents it from being applied.

In particular, it does not allow this migration to be faked because other migrations that depend on it, i.e., any migrations that include references to settings.AUTH_USER_MODEL, have already been run.

I’m not entirely sure why Django places this restriction on faking migrations, since the whole point is to tell it that the migration has, in fact, already been applied (if you know why, please comment below).

Instead, you can accomplish the same result by adding the initial migration for your new users app to the migration history by hand :

echo "INSERT INTO django_migrations (app, name, applied)
VALUES ('users', '0001_initial', CURRENT_TIMESTAMP);" | python manage_dev.py dbshell

or if you do not have mysql (you are in a docker image for example):

INSERT INTO django_migrations (app, name, applied)
VALUES ('users', '0001_initial', CURRENT_TIMESTAMP);

with DBeaver for example.

../../../../../../_images/dbeaver_migration_users.png

After this SQL command, Django sees the migration:

python manage_dev.py showmigrations
users
 [X] 0001_initial

If you’re using an app name other than users , replace users in the line above with the name of the Django app that holds your user model.

At the same time, let’s update the django_content_types table with the new app_label for our user model, so existing references to this content type will remain intact.

As with the prior database change, this change must be made before running migrate.

The reason for this is that migrate will create any non-existent content types, which will then prevent you from updating the old content type with the new app label (with a “duplicate key value violates unique constraint” error).

echo "UPDATE django_content_type SET app_label = 'users'
WHERE app_label = 'auth' and model = 'user';" | python manage.py dbshell

or if you do not have mysql (you are in a docker image for example):

UPDATE django_content_type SET app_label = 'users'
WHERE app_label = 'auth' and model = 'user';
../../../../../../_images/dbeaver_django_content_type.png

Again, if you called your app something other than users , be sure to update SET app_label = ‘users’ in the above with your chosen app name.

At this point, you should stop and deploy everything to a staging environment , as attempting to run migrate before manually tweaking your migration history will fail.

If your automated deployment process runs migrate (which it likely does), you will need to update that process to run these two SQL statements before migrate (in particular because migrate will create any non-existent content types for you, thereby preventing you from updating the existing content type in the database without further fiddling).

Test this process thoroughly (perhaps even multiple times) in a staging environment to make sure you have everything automated correctly.

7) shell_plus

We can check that we have not lost our old Django users.

python manage.py shell_plus
In [2]: users = User.objects.all()

In [3]: users
Out[3]: <QuerySet [<User: root>, <User: admin>]>