I've come to a point where I had to swap django's user model with a custom one, at a point when I already had users, and already had apps depending on the model. Therefore this guide is trying to provide the support that is not found in the django docs.
Django warns that this should only be done basically at the start of a project, so that the initial migration of an app includes the creation of the custom user model.
It took a while to do, and I ran into problems created by the already existing relations between other models and auth.User
.
There were good and not so good things regarding my project state, that influenced the difficulty of the job.
- My custom user also had an
id
field, that's just a usual default django id - All the apps that depended on the user, specified the dependency nicely (meaning
ForeignKey(settings.AUTH_USER_MODEL...)
- I had edit access to the apps that didn't specify this dependency nicely (some of my own apps), so I could make them act nice.
- I had a pretty simple new model. By simple, I mean it didn't declare any ForeignKey fields to other models in my app. If your model has to depend on others, but you'll be creating your model from scratch, you're ok as long as you declare those relationships only after you have carried out all the steps here - simple enough to do.
- I could port my users from the old table to the new one easily, because they had the same structure
- I had no generic relations to the
auth.User
model. (Fixing the generic relations would have been simple, though tedious - I would have had to update all the references to theContentType
of theauth.User
to point to the content type ofmyapp.User
)
- I had to subclass Abstractuser
- I had custom apps depending on
auth.User
, all complete with saved models and everything.
- You can create models with a specified
id
- You have to edit migration 0001_initial
- When inheriting from AbstractUser, you have to fix some reverse relation name collisions (for example, the
auth.User.groups
field implies anauth.Group.users
field; if myapp.User inherits from AbstractUser, your model will try to also create its own.groups
field, which will also require aauth.Group.users
field - problem - the same field name can't refer to 2 models) - The same name collision happens on
auth.Group
(and alsoauth.Permission
) on the related query name, BUT these are NOT reported to you by the django check, so you must be carefull!!
At some point during this process, when running django migrations, django will ask you whether to discard some stale content types. I didn't do it, but I'm not sure what would have hapenned if I had, so I advise against it.
- Create a custom AbstractUser.
from django.contrib.auth.models import AbstractUser as _DjangoAbstractUser
class AbstractUser(_DjangoAbstractUser):
class Meta:
abstract = True
# Need to do this after the class object is created, because we need _meta access
AbstractUser._meta.get_field_by_name('groups')[0].rel.related_name = 'special_users'
AbstractUser._meta.get_field_by_name('groups')[0].rel.related_query_name = 'special_users'
AbstractUser._meta.get_field_by_name('user_permissions')[0].rel.related_name = 'special_users'
AbstractUser._meta.get_field_by_name('user_permissions')[0].rel.related_query_name = 'special_users'
class User(AbstractUser):
# you know what to put here BUT
# DO NOT create foreign key or m2m relations to other models at this step.
# You can create those later
pass
- Run
django-admin makemigrations myapp
. This migration will have to be deployed on all your environments, and applied. It will later be deleted, the file, and the history of it being applied, from thedjango_migrations
table manually. Things will get weird, so hold on. - After this, apply the migration
django-admin migrate myapp
, to create your database table. Run this on all environments, and ONLY AFTER THAT carry on with the next steps. - In this newly created migration, there's an operations list. In it, you'll see your new model's definition. It should look similar to this.
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)),
...
('groups', models.ManyToManyField(related_query_name='special_users', related_name='special_users', to='auth.Group', blank=True, help_text='....', verbose_name='groups')),
('user_permissions', models.ManyToManyField(related_query_name='special_users', related_name='special_users', to='auth.Permission', blank=True, help_text='...', verbose_name='user permissions')),
],
options={
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
Copy this operation in your app's 0001_initial migration's operation list (I did it at the end, but it doesn't matter).
- The previously created migration also specified some dependencies, most likely on the
django.contrib.auth
migrations . My migration depended on('auth', '0006_require_contenttypes_0002'),
, but of course it can differ from you if you're reading this in the future. Copy this dependency on theauth
app in the list of dependencies of the 0001_initial migration. (It also had a dependency on your app's previous migration, but this can be ignored if this new user model shouldn't actually depend on any of your other app's models at this step.). - Delete the file containing this newly generated and migrated app! As crazy as it sounds, at least you're now done with the hard part of the schema migration! Congrats! Deploy and carry out the next steps these on all environments!
- Delete from the DB migration history table (calld
django_migrations
) the entry that says you ever applied this migration (ON ALL ENVIRONMENTS) - Optional, but most likely required every time: The data migration. If you're lucky like me, and your new user model also has an
id
field, you'll be able to port your users and all their relations with minimum ease. Just create a new empty migration, and create aRunPython
class that does this as a forward move.Of course, this migration won't allow you to migrate backwards, but if you want to ever do it, just make a backwards function that does the exact reverse of this 'forwards` function. Notice, i haven't deleted the old users. You can do that if you want, either in the migration, by hand, or however.def forward(apps, schema_editor): old_user_model = apps.get_model('auth', 'User') new_user_model = apps.get_model('myapp', 'User') for old_user in old_user_model.objects.all(): # simply copy the fields you need into the new user. the `id` is the most important field new_user = new_user_model.objects.create( id=old_user.id, # this is a very important thing to do, because of the generic relations, username=old_user.username, # dunno if you need this, put whatever fields you want here password=old_user.password # works like this, of course. And of course, i don't know if you need this # ckeck out django's PermissionsMixin, AbstractUser and AbstractBaseUser for all the fields # that the old user model had ... ) new_user.groups.add(*old_user.groups.all()) new_user.user_permissions.add(*old_user.user_permissions.all()) new_user.save() class Migration(migrations.Migration): dependencies = [...] operations = [ RunPython(forward) ]
- Commit this migration, deploy and run it on all your envs. This migration will stay, but be carefull, because running it multiple times is not posible in this state. Use
get_or_create
instead ofcreate
if you plan on running it multiple times. - You can now finally swap the user model and insert in your settings file
AUTH_USER_MODEL = 'myapp.User'
. Deploy this new settings file on al environments. - Done! Congrats! Now you can make any additional changes to your model (like specifying custom relations to other models)
If you ever want to swap back, bear in mind that auth.User
only has a manager when that AUTH_USER_MODEL
is the default one. That means, you can't access any 'old' after you swapped. You need to have migrated them before that point.
This doesn't seem to account for all the 3rd party apps with FKs to the User table. You've accounted for only Groups and Permissions in your data migration.