Nokankoputuksia

Nokankoputuksia

Tämä blogi on tarkoitettu kaikille, jotka haluavat oppia ymmärtämään paremmin sähköistä liiketoimintaa.

Custom user model in Django 1.5 – Step-by-step migration guide

3
Django

Amongst the new features introduced in Django version 1.5 is the much awaited ability to use a custom user model in place of the standard one. In this article we describe how we migrated an existing project from Django’s built-in User model + project specific profile model to a single custom User model.

Read on for details.

Custom user model explained

Prior to Django 1.5, using the authentication framework – or anything relying on it, such as the admin site – meant using a one-size-fits-all user model. The way to associate extra data with the model was to create an extra table with a one-to-one key pointing to a row in the main user table. As of Django 1.5, it is finally possible to substitute the standard User model with your own.

So, why migrate an existing application, seeing as the old method can still be used? Reasons include:

  • Simpler code: no need for user.get_profile()
  • Simpler database structure: one less table to worry about
  • Simpler queries: no need to JOIN two tables to get all the user properties
  • Usernames are not restricted to 30 characters.

The last bullet is especially interesting. In our project, we use email addresses as usernames; a fairly common thing to do today. However, an email address is quite often longer than 30 characters, and this is a problem when we are limited to Django’s bundled user model. The limitation could be worked around by creating  a custom authentication backend for logging in with the email address. With a fully customized user model, we can drop the username field and use the email field as the username directly.

Creating the new user model

The first step in the migration process is to create the new user model. Our old UserProfile model looks like this (irrelevant details omitted):

For login, we use only the email field, so we also have a custom authentication backend and have added a UNIQUE constraint to auth_user table’s email column. The username field itself is set to some random unique value. This hacky workaround is soon history.

We’ll build the new User class by copying code from Django’s AbstractUser class and our old UserProfile (again, extraneous details omitted):

We do not inherit from AbstractUser, as that would bring in the unwanted username field. Instead, we inherit from AbstractBaseUser, which includes only the most basic functionality, such as the is_authenticated() and set_password() methods. We still want to use Django’s groups and permissions, so we inherit from PermissionsMixin as well.

The required USERNAME_FIELD attribute tells which field to use as the username during authentication.

Because our model doesn’t have a field named ”username”, we must also reimplement UserManager:

Since the create_user method has changed, calling code must be updated. In our case, this was also an opportunity for simplification, as we no longer need to come up with a unique pseudorandom username that isn’t even used.

To inform Django that we want to use our new custom User module, the AUTH_USER_MODEL setting must be set:

  AUTH_USER_MODEL = 'user.User'

The old setting AUTH_PROFILE_MODULE should be removed.

In the old version, logging in with the email address as the username was accomplished with a custom authentication backend. This is no longer needed, so we can take out the AUTHENTICATION_BACKENDS line as well.

Updating the code

With the new user model created, we can update the rest of our codebase to match.

First, we must update all ForeignKey references. Finding said references is as easy as

  git grep 'ForeignKey(User'

The update can be as simple as changing the import statement to point to our own User model, or we can follow Django documentation and use AUTH_USER_MODEL from django.conf.settings. Using AUTH_USER_MODEL is obviously the right choice for reuable apps, but for a project’s core apps, which are more or less tightly coupled to the custom User implementation anyway, I don’t really see much benefit.

The next step is to root out any references to the old UserProfile class:

  git grep UserProfile

Replacing UserProfile with User should be enough. To break circular dependencies or just to write more reusable code, refer to the user model class indirectly with get_user_model() from django.contrib.auth.

The user profile can also have been used in queries so grepping for ”userprofile” (case sensitive) is also needed. Here is the greatest opportunity to optimize and simplify queries, as we are removing a JOIN.

Finally, the most common way to access the profile is via the get_profile() method. In our case, we’ve monkeypatched the standard User model with an extra ”profile” property
that automatically creates the UserProfile if not found. Grepping for ”\.profile” or ”\.get_profile()” reveals where profile references are made. In most of these cases, we can simply remove the ”.profile” part.

Updating the Admin site

We use Django’s admin app in this project, so a new admin model must be created for our custom user model. We’ll start by creating a new ModelAdmin that inherits from django.contrib.auth.admin.UserAdmin. At minimum, we must override all attributes that reference the ’username’ field our custom model lacks. Looking at the UserAdmin source, we see that at least these attributes must be overridden: fieldsets, add_fieldsets, form, add_form, list_display, search_fields and ordering.

Overriding the necessary fields and moving our custom additions from the old UserProfile admin class, we end up with something like this:

Notice the formand add_form attributes. These are ModelForms and the default ones are for the wrong model. The main difference is that the username field is replaced with the email field. Like so:

The UserChangeForm can also be copied straight from Django source with little modifications. The username field was dropped and the model was changed to point to our custom User.

Updating the database

At last, we get to the most perilous part of the upgrade: the database migration.

First, we create the new User table with syncdb. Our model is in the ”user” app, so the table will be named ”user_user”. Since all our foreign keys point to auth_user instead of user_userprofile (well, all but one), we can retain IDs from auth_user, greatly simplifying the migration.

We start by populating our brand new user_user table with the combined contents of auth_user and user_userprofile:

Next, we need to change all foreign keys pointing to auth_user.id to user_user.id. We can get the list of such foreign key constraints with this query:

We update each foreign key by dropping the old constraint and replacing it with a new one. In that one table with a foreign key to user_userprofile instead of  auth_user, we need to update the IDs before re-adding the constraint.

One thing that is easy to forget is the user groups many-to-many helper table. Changing the foreign key won’t help there, as the name of the table has changed as well. The syncdb command we ran earlier created the new user_user_groups table as well, so moving over the data can be done with a single INSERT query:

The database migration is now done. Once we are satisfied everything works, the old tables can be safely dropped.

Conclusion

To summarize, we:

  • Created a new User model
  • Updated user creation code
  • Created new admin models and forms for the custom User model
  • Removed a custom authentication backend that was no longer needed
  • Changed all references from the old User and UserProfile to the new User
  • Populated the new user_user table with the combined contents of auth_user and user_userprofile
  • Changed all foreign keys to point to user_user instead of auth_user

With these changes, the migration should be complete! Just remember to test thoroughly before pushing to production!

p.s. Check out my previous blog pots about Django formtags also.

Jos pidit lukemastasi tai sinulla on lisättävää kommentoi.


  • Retselisitsoe Moabi

    Thank you very much, very informative.

  • gamesbook

    Hi – all the queries are missing … please can you add them back!

  • gamesbook

    The queries are back, thanks. But now I get the error, for the SQL for the list of such foreign key constraints:
    ERROR 1109 (42S02): Unknown table ’constraint_column_usage’ in information_schema