August 7, 2013
by Tobias McNulty
0 comments
Categories:
Technical

Migrating to a Custom User Model in Django

The new custom user model configuration that arrived in Django makes it relatively straightforward to swap in your own model for the Django user model. In most cases, Django's built-in User model works just fine, but there are times when certain limitations (such as the length of the email field) require a custom user model to be installed. If you're starting out with a custom user model, setup and configuration are relatively straightforward, but if you need to migrate an existing legacy project (e.g., one that started out in Django 1.4 or earlier), there are a few gotchas that you might run into. We did this recently for one of our larger, long-term client projects at Caktus, and here's an outline of how we'd recommend tackling this issue:

  1. First, assess any third party apps that you use 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.

  2. 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, you can settings.AUTH_USER_MODEL (which is just 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), due to circular reference issues. Generally speaking 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 references you'll have to stumble your way through.

    This is also a good time to add the AUTH_USER_MODEL setting to your settings.py, just for the sake of explicitness:

# settings.py

AUTH_USER_MODEL = 'auth.User'
  1. Now that you've done a good bit of the leg work, you can turn to actually creating the custom user model itself. How this looks is obviously up to you, but here's a sample that duplicates the functionality of Django's built-in User model, removes the username field, and extends the length of the email field
# appname/models.py

from django.db import models
from django.utils import timezone
from django.utils.http import urlquote
from django.utils.translation import ugettext_lazy as _
from django.core.mail import send_mail
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin

class CustomUser(AbstractBaseUser, PermissionsMixin):
    """
    A fully featured User model with admin-compliant permissions that uses
    a full-length email field as the username.

    Email and password are required. Other fields are optional.
    """
    email = models.EmailField(_('email address'), max_length=254, unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    is_staff = models.BooleanField(_('staff status'), default=False,
        help_text=_('Designates whether the user can log into this admin '
                    'site.'))
    is_active = models.BooleanField(_('active'), default=True,
        help_text=_('Designates whether this user should be treated as '
                    'active. Unselect this instead of deleting accounts.'))
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)

    objects = CustomUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_absolute_url(self):
        return "/users/%s/" % urlquote(self.email)

    def get_full_name(self):
        """
        Returns the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        "Returns the short name for the user."
        return self.first_name

    def email_user(self, subject, message, from_email=None):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email])

Note that this duplicates all aspects of the built-in Django User model except the get_profile() method, which you may or may not need in your project. Unless you have third party apps that depend on it, it's probably easier simply to extend the custom user model itself with the fields that you need (since you're already overriding it) than to rely on the older get_profile() method. It is worth noting that, unfortunately, since Django does not support overriding model fields, you do need to copy all of this from the AbstractUser class within django.contrib.auth.models rather than simply extending and overriding the email field.

  1. You might have noticed the Manager specified in the model above doesn't actually exist yet. In addition to the model itself, you need to create a custom manager that supports methods like create_user(). Here's a sample manager that creates users without a username (just an email):
# appname/models.py

from django.contrib.auth.models import BaseUserManager

class CustomUserManager(BaseUserManager):

    def _create_user(self, email, password,
                     is_staff, is_superuser, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        now = timezone.now()
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email,
                          is_staff=is_staff, is_active=True,
                          is_superuser=is_superuser, last_login=now,
                          date_joined=now, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

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

    def create_superuser(self, email, password, **extra_fields):
        return self._create_user(email, password, True, True,
                                 **extra_fields)
  1. If you plan to edit users in the admin, you'll most likely also need to supply custom forms for your new user model. In this case, rather than copying and pasting the complete forms from Django, you can extend Django's built-in UserCreationForm and UserChangeForm to remove the username field (and optionally add any others that are required) like so:
# appname/forms.py

from django.contrib.auth.forms import UserCreationForm, UserChangeForm

from appname.models import CustomUser

class CustomUserCreationForm(UserCreationForm):
    """
    A form that creates a user, with no privileges, from the given email and
    password.
    """

    def __init__(self, *args, **kargs):
        super(CustomUserCreationForm, self).__init__(*args, **kargs)
        del self.fields['username']

    class Meta:
        model = CustomUser
        fields = ("email",)

class CustomUserChangeForm(UserChangeForm):
    """A form for updating users. Includes all the fields on
    the user, but replaces the password field with admin's
    password hash display field.
    """

    def __init__(self, *args, **kargs):
        super(CustomUserChangeForm, self).__init__(*args, **kargs)
        del self.fields['username']

    class Meta:
        model = CustomUser

Note that in this case we do not use the generic accessors for the user model; rather, we import the CustomUser model directly since this form is tied to this (and only this) model. The benefit of this approach is that it also allows you to test your model via the admin in parallel with your existing user model, before you migrate all your user data to the new model.

  1. Next, you need to create a new admin.py entry for your user model, mimicking the look and feel of the built-in admin as needed. Note that for the admin, similar to what we did for forms, you can extend the built-in UserAdmin class and modify only the attributes that you need to change, keeping the other behavior intact.
# appname/admin.py

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

from appname.models import CustomUser
from appname.forms import CustomUserChangeForm, CustomUserCreationForm

class CustomUserAdmin(UserAdmin):
    # The forms to add and change user instances

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference the removed 'username' field
    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 = CustomUserChangeForm
    add_form = CustomUserCreationForm
    list_display = ('email', 'first_name', 'last_name', 'is_staff')
    search_fields = ('email', 'first_name', 'last_name')
    ordering = ('email',)

admin.site.register(CustomUser, CustomUserAdmin)
  1. Once you're happy with the fields in your model, use South to create the schema migration to create your new table:
python manage.py schemamigration appname --auto
  1. This is a good point to pause, check out your user model via the admin, and make sure it looks and functions as expected. You should still see both user models at this point, because we haven't yet adjusted the AUTH_USER_MODEL setting to point to our new model (this is intentional). You may have to delete the migration file and repeat the previous step a few times if you don't get it quite right the first time.
  2. Next, we need to write a data migration using South to copy the data from our old user model to our new user model. This is relatively straightforward, and you can get a template for the data migration as follows:
python manage.py datamigration appname --freeze otherapp1 --freeze otherapp2

Note that the --freeze arguments are optional and should be used only if you need to access the models of these other apps in your data migration. If you have foreign keys in these other apps to Django's built-in auth.User model, you'll likely need to include them in the data migration. Again, you can experiment and repeat this step until you get it right, deleting the incorrect migrations as you go.

  1. Once you have the template for your data migration created, you can write the content for your migration. A simple forward migration to simply copy the users, maintaining primary key IDs (this has been verified to work with PostgreSQL but no other backend), might look something like this:
# appname/migrations/000X_copy_auth_user_data.py

class Migration(DataMigration):
    def forwards(self, orm):
        "Write your forwards methods here."
        for old_u in orm['auth.User'].objects.all():
            new_u = orm.CustomUser.objects.create(
                        date_joined=old_u.date_joined,
                        email=old_u.email and old_u.email or '%s@example.com' % old_u.username,
                        first_name=old_u.first_name,
                        id=old_u.id,
                        is_active=old_u.is_active,
                        is_staff=old_u.is_staff,
                        is_superuser=old_u.is_superuser,
                        last_login=old_u.last_login,
                        last_name=old_u.last_name,
                        password=old_u.password)
            for perm in old_u.user_permissions.all():
                new_u.user_permissions.add(perm)
            for group in old_u.groups.all():
                new_u.groups.add(group)

Since we ensure that the primary keys stay the same from one table to another, we can just adjust the foreign keys in our other models to point to this new custom user model, rather than needing to update each row in turn.

Note 1: This migration does not account for any duplicate emails that exist in the database, so if this is a problem in your case, you may need to write a separate migration first that resolves any such duplicates (and/or manually resolve them with the users in question).

Note 2: This migration does not update the content types for any generic relations in your database. If you use generic relations and one or more of them points to the old user model, you'll also need to update the content type foreign keys in these relations to reference the content type of the new user model.

  1. Once you have this migration written and tested to your liking (and have resolved any duplicate user issues), you can run the migration and verify that it did what you expected via the Django admin.
python manage.py migrate

Needless to say, we recommend doing this testing on a local, development copy of the production database (using the same database server) rather than on a live production or even staging server. Once you have the entire process complete, you can test it on a staging (and finally on the production) server.

  1. Before we switch to our new model, let's create a temporary initial migration for the auth app which we can later use as the basis for creating a migration to delete the obsolete auth_user and related tables. First, create a temporary module for auth migrations in your settings file:
SOUTH_MIGRATION_MODULES = {
    'auth': 'myapp.authmigrations',
}

Then, "convert" the auth app to South like so:

python manage.py convert_to_south auth

This won't do anything to your database, but it will create (and fake the run of) an initial migration for the auth app.

  1. Let's review where we stand. You've (a) updated all your code to use the generic interface for accessing the user model, (b) created a new model and the corresponding forms to access it through the admin, and (c) written a data migration to copy your user data to the new model. Now it's time to make the switch. Open up your settings.py and adjust the AUTH_USER_MODEL setting to point to the appname.ModelName path of your new model:
# settings.py

AUTH_USER_MODEL = 'appname.CustomModel'
  1. Since any foreign keys to the old auth.User model have now been updated to point to your new model, we can create migrations for each of these apps to adjust the corresponding database tables as follows:
python manage.py schemamigration --auto otherapp1

You'll need to repeat this for each of the apps in your project that were affected by the change in the AUTH_USER_MODEL setting. If this includes any third party apps, you may want to store those migrations in an app inside your project (rather than use the SOUTH_MIGRATION_MODULES setting) so as not to break future updates to those apps that may include additional migrations.

  1. Additionally, at this time you'll likely want to create the previously-mentioned South migration to delete the old auth_user and associated tables from your database:
python manage.py schemamigration --auto auth

Since South is not actually intended to work with Django's contrib apps, you need to copy the migration this command creates into the app that contains your custom model, renumbering it along the way to make sure that it's run after your data migration to copy your user data. Once you have that created, be sure to delete the SOUTH_MIGRATION_MODULES setting from your settings file and remove the unneeded, initial migration from your file system.

  1. Last but not least, let's run all these migrations and make sure that all the pieces work together as planned:
python manage.py migrate

Note: If you get prompted to delete a stale content type for auth | user, don't answer "yes" yet. Despite Django's belief to the contrary, this content type is not actually stale yet, because the South migration to remove the auth.User model has not yet run. If you accidentally answer "yes," you might see an error stating InternalError: current transaction is aborted, commands ignored until end of transaction block. No harm was done, just run the command again and answer "no" when prompted. If you'd like to remove the stale auth | user content type, just run python manage.py syncdb again after all the migrations have completed.

That completes the tutorial on creating (and migrating to) a custom user model with Django 1.5's new support for the same. As you can see it's a pretty involved process, even for something as simple as lengthening and requiring a unique email field in place of a username, so it's not something to be taken lightly. Nonetheless, some projects can benefit from investing in a custom user model that affords all the necessary flexibility. Hopefully this post will help you make that decision (and if needed the migration itself) with all the information necessary to make it go as smoothly as possible.

blog comments powered by Disqus