Skip to content

Instantly share code, notes, and snippets.

@vladiibine
Last active March 9, 2020 20:59
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save vladiibine/edafbfd929254eb72f76 to your computer and use it in GitHub Desktop.
Save vladiibine/edafbfd929254eb72f76 to your computer and use it in GitHub Desktop.
Swap django auth.User with custom model after having applied the 0001_initial migration

Swapping the django user model during the lifecycle of a project (django 1.8 guide)

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.

Things that made the swap simpler
  1. My custom user also had an id field, that's just a usual default django id
  2. All the apps that depended on the user, specified the dependency nicely (meaning ForeignKey(settings.AUTH_USER_MODEL...)
  3. 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.
  4. 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.
  5. I could port my users from the old table to the new one easily, because they had the same structure
  6. 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 the ContentType of the auth.User to point to the content type of myapp.User)
Things that made the swap mode complicated
  1. I had to subclass Abstractuser
  2. I had custom apps depending on auth.User , all complete with saved models and everything.
Key points that I learned
  1. You can create models with a specified id
  2. You have to edit migration 0001_initial
  3. When inheriting from AbstractUser, you have to fix some reverse relation name collisions (for example, the auth.User.groups field implies an auth.Group.users field; if myapp.User inherits from AbstractUser, your model will try to also create its own .groups field, which will also require a auth.Group.users field - problem - the same field name can't refer to 2 models)
  4. The same name collision happens on auth.Group (and also auth.Permission) on the related query name, BUT these are NOT reported to you by the django check, so you must be carefull!!
Before you start:

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.

Steps that I took
  1. 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
  1. 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 the django_migrations table manually. Things will get weird, so hold on.
  2. 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.
  3. 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).
  1. 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 the auth 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.).
  2. 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!
  3. Delete from the DB migration history table (calld django_migrations) the entry that says you ever applied this migration (ON ALL ENVIRONMENTS)
  4. 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 a RunPython class that does this as a forward move.
    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)
     ]
    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.
  5. 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 of create if you plan on running it multiple times.
  6. 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.
  7. Done! Congrats! Now you can make any additional changes to your model (like specifying custom relations to other models)
Going back

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.

@spookylukey
Copy link

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.

@vladiibine
Copy link
Author

From my tests, the fact that I created users with the same id's as the auth.User instance IDs solves this problem without doing anything more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment