Migrating to a Custom User Model in Django
UPDATE: Read a newer blog post on this topic.
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:
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.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 theget_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 cansettings.AUTH_USER_MODEL
(which is just a string corresponding to theappname.ModelName
path to the user model).Note that
get_user_model()
cannot be called at the module level in anymodels.py
file (and by extension any file that amodels.py
imports), due to circular reference issues. Generally speaking it's easier to keep calls toget_user_model()
inside a method whenever possible (so it's called at run time rather than load time), and usesettings.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 yoursettings.py
, just for the sake of explicitness:# settings.py AUTH_USER_MODEL = 'auth.User'
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 theusername
field, and extends the length of theemail
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.
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)
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
andUserChangeForm
to remove theusername
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.
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-inUserAdmin
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)
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
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.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.
- 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.
- 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.
- 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 obsoleteauth_user
and related tables. First, create a temporary module forauth
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.
- 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 theAUTH_USER_MODEL
setting to point to theappname.ModelName
path of your new model:
# settings.py AUTH_USER_MODEL = 'appname.CustomModel'
- 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.
- 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.
- 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.