Caktus CEO Tobias McNulty working at his desk

The Django documentation recommends always starting your project with a custom user model (even if it's identical to Django's to begin with), to make it easier to customize later if you need to. But what are you supposed to do if you didn't see this when starting a project, or if you inherited a project without a custom user model and you need to add one?

At Caktus, when Django first added support for a custom user model, we were still using South for migrations. Hard to believe! Nearly six years ago, I wrote a post about migrating to a custom user model that is, of course, largely obsolete now that Django has built-in support for database migrations. As such, I thought it would be helpful to put together a new post for anyone who needs to add a custom user model to their existing project on Django 2.0+.

Background

As of the time of this post, ticket #25313 is open in the Django ticket tracker for adding further documentation about this issue. This ticket includes some high-level steps to follow when moving to a custom user model, and I recommend familiarizing yourself with this first. As noted in the documentation under Changing to a custom user model mid-project, "Changing AUTH_USER_MODEL after you’ve created database tables is significantly more difficult since it affects foreign keys and many-to-many relationships, for example."

The instructions I put together below vary somewhat from the high-level instructions in ticket #25313, I think (hope) in positive and less destructive ways. That said, there's a reason this ticket has been open for more than four years — it’s hard. So, as mentioned in the ticket:

Proceed with caution, and make sure you have a database backup (and a working process for restoring it) before changing your production database.

Overview

Steps 1 and 2 below are the same as they were in 2013 (circa Django 1.5), and everything after that differs since we're now using Django's built-in migrations (instead of South). At a high level, our strategy is to create a model in one of our own apps that has all the same fields as auth.User and uses the same underlying database table. Then, we fake the initial migration for our custom user model, test the changes thoroughly, and deploy everything up until this point to production. Once complete, you'll have a custom user model in your project, as recommended in the Django documentation, which you can continue to tweak to your liking.

Contrary to some other methods (including my 2013 post ), I chose this time to update the existing auth_user table to help ensure existing foreign key references stay intact. The downside is that it currently requires a little manual fiddling in the database. Still, if you're using a database with referential integrity checking (which you should be), you'll sleep easier at night knowing you didn't mess up a data migration affecting all the users in your database.

If you (and a few others) can confirm that something like the below works for you, then perhaps some iteration of this process may make it into the Django documentation at some point.

Migration Process

Here's my approach for switching to a custom user model mid-project:

  1. Assumptions:

    • You have an existing project without a custom user model.
    • You're using Django's migrations, and all migrations are up-to-date (and have been applied to the production database).
    • You have an existing set of users that you need to keep, and any number of models that point to Django's built-in User model.
  2. First, assess any third party apps to make sure they either don't have any references to the Django's User model, or if they do, that they use Django's generic methods for referencing the user model.

  3. Next, do the same thing for your own project. Go through the code looking for any references you might have to the User model, and replace them with the same generic references. In short, you can use the get_user_model() method to get the model directly, or if you need to create a ForeignKey or other database relationship to the user model, use settings.AUTH_USER_MODEL (which is simply a string corresponding to the appname.ModelName path to the user model).

    Note that get_user_model() cannot be called at the module level in any models.py file (and by extension any file that a models.py imports), since you'll end up with a circular import. Generally, it's easier to keep calls to get_user_model() inside a method whenever possible (so it's called at run time rather than load time), and use settings.AUTH_USER_MODEL in all other cases. This isn't always possible (e.g., when creating a ModelForm), but the less you use it at the module level, the fewer circular imports you'll have to stumble your way through.

  4. 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
    
  5. 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'
    
  6. 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)
    
  7. In settings.py, add users to INSTALLED_APPS and set AUTH_USER_MODEL = 'users.User':

    INSTALLED_APPS = [
        # ...
        'users',
    ]
    
    AUTH_USER_MODEL = 'users.User'
    
  8. Create an initial migration for your new User model:

    python manage.py makemigrations
    

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

  9. 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.py dbshell
    

    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
    

    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.

    Note that this SQL is for Postgres, and may vary somewhat for other database backends.

  10. 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.

  11. After testing and fixing any errors, everything up until this point should be deployed to production (and/or any other environments where you need to keep the existing user database), after ensuring that you have a good backup and a process for restoring it in the event anything goes wrong.

  12. Now, you should be able to make changes to your users.User model and run makemigrations / migrate as needed. For example, as a first step, you may wish to rename the auth_user table to something in your users app's namespace. You can do so by removing db_table from your User model, so it looks like this:

    class User(AbstractUser):
        pass
    

    You'll also need to create and run a new migration to make this change in the database:

    python manage.py makemigrations --name rename_user_table
    python manage.py migrate
    

Success?

That should be it. You should now be able to make other changes (and create migrations for those changes) to your custom User model. The types of changes you can make and how to go about making those changes is outside the scope of this post, so I recommend carefully reading through the Django documentation on substituting a custom User model. In the event you opt to switch from AbstractUser to AbstractBaseUser, be sure to create data migrations for any of the fields provided by AbstractUser that you want to keep before deleting those columns from your database. For more on this topic, check out our post about DjangoCon 2017, where we link to a talk by Julia M Looney titled "Getting the most out of Django’s User Model." My colleague Dmitriy also has a great post with some other suggestions for picking up old projects.

Once again, please test this carefully in a staging environment before attempting it on production, and make sure you have a working database backup. Good luck, and please comment below with any success or failure stories, or ideas on how to improve upon this process!

New Call-to-action
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times