#25313: Document how to migrate from a built-in User model to a custom User 
model
-------------------------------+------------------------------------
     Reporter:  Carl Meyer     |                    Owner:  nobody
         Type:  New feature    |                   Status:  new
    Component:  Documentation  |                  Version:  1.8
     Severity:  Normal         |               Resolution:
     Keywords:                 |             Triage Stage:  Accepted
    Has patch:  0              |      Needs documentation:  0
  Needs tests:  0              |  Patch needs improvement:  0
Easy pickings:  0              |                    UI/UX:  0
-------------------------------+------------------------------------

Comment (by johanneswilm):

 The following approach has not been tested with all database backends
 (only sqlite), but it seems to work by only using migrations, no raw SQL
 and not destroying any existing migrations. This is based of Tobias
 McNulty's walk-through [1], with some modifications to avoid doing
 anything "nasty".


 ----

 0. Assumptions:

 * You have an existing project without a custom user model.
 * You're using Django's migrations, and all migrations are up-to-date (and
 have been applied to the production database).
 * You have an existing set of users that you need to keep, and any number
 of models that point to Django's built-in User model.
 * **You have an existing django app with existing migrations to which you
 want to add the custom user model.**
 * **You need the same code to run both for new installations of your code
 base and for existing ones.

 1. First, assess any third party apps 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, use
 settings.AUTH_USER_MODEL (which is simply 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), since
 you'll end up with a circular import. Generally, 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 imports
 you'll have to stumble your way through.

 3. In the existing app in which you want the custom User model to live (we
 will call it "user" in this example), add the following to the models.py
 file:

 {{{
 from django.db import models
 from django.contrib.auth.models import AbstractUser


 class User(AbstractUser):
     class Meta:
         db_table = 'auth_user'
 }}}

 4. Edit the migration with a name starting with "0001" of this app.

 Under "operations" paste the following as the first item:

 {{{
 migrations.CreateModel(
             name='User',
             fields=[
                 ('id', models.AutoField(auto_created=True,
 primary_key=True, serialize=False, verbose_name='ID')),
                 ('password', models.CharField(max_length=128,
 verbose_name='password')),
                 ('last_login', models.DateTimeField(blank=True, null=True,
 verbose_name='last login')),
                 ('is_superuser', models.BooleanField(default=False,
 help_text='Designates that this user has all permissions without
 explicitly assigning them.', verbose_name='superuser status')),
                 ('username', models.CharField(error_messages={'unique': 'A
 user with that username already exists.'}, help_text='Required. 150
 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150,
 unique=True,
 validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
 verbose_name='username')),
                 ('first_name', models.CharField(blank=True,
 max_length=150, verbose_name='first name')),
                 ('last_name', models.CharField(blank=True, max_length=150,
 verbose_name='last name')),
                 ('email', models.EmailField(blank=True, max_length=254,
 verbose_name='email address')),
                 ('is_staff', models.BooleanField(default=False,
 help_text='Designates whether the user can log into this admin site.',
 verbose_name='staff status')),
                 ('is_active', models.BooleanField(default=True,
 help_text='Designates whether this user should be treated as active.
 Unselect this instead of deleting accounts.', verbose_name='active')),
                 ('date_joined',
 models.DateTimeField(default=django.utils.timezone.now, verbose_name='date
 joined')),
                 ('groups', models.ManyToManyField(blank=True,
 help_text='The groups this user belongs to. A user will get all
 permissions granted to each of their groups.', related_name='user_set',
 related_query_name='user', to='auth.Group', verbose_name='groups')),
                 ('user_permissions', models.ManyToManyField(blank=True,
 help_text='Specific permissions for this user.', related_name='user_set',
 related_query_name='user', to='auth.Permission', verbose_name='user
 permissions')),
             ],
             options={
                 'db_table': 'auth_user',
             },
             managers=[
                 ('objects', django.contrib.auth.models.UserManager()),
             ],
         ),
 }}}

 Under "dependencies" add:
 {{{
 ('auth', '0012_alter_user_first_name_max_length'),
 }}}

 and, if present, remove

 {{{
 migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 }}}

 (The exact code to use here will likely change over time with newer
 versions of Django. You can find the current code by creating a new app
 temporarily, add the User model to it and then look at the migration file
 `./manage.py makemigrations` produces.)

 5. Create a new data migration to the user app by typing: `./manage.py
 makemigrations --empty user`

 6. Edit the newly created migration:

 Under "operations" add `migrations.RunPython(change_user_type),`

 Add this function to the top of the file:

 {{{
 def change_user_type(apps, schema_editor):
     ContentType = apps.get_model('contenttypes', 'ContentType')
     ct = ContentType.objects.filter(
         app_label='auth',
         model='user'
     ).first()
     if ct:
         ct.app_label = 'user'
         ct.save()
 }}}

 7. You can now migrate to the custom user model by running `./manage.py
 migrate`. This should always work, however, it will do so in two different
 ways depending on whether it is run on an existing or a new instance:

 * On a new instance, the new user model will be created in migration 0001.
 The last migration will have no effect.

 * On an existing instance, the migration 0001 will be ignored as the
 system has already previously applied migration 0001 and will therefore
 now ignore it. Instead the last migration will have the effect of change
 the app of the user model.

 8. Now, you should be able to make changes to your users.User model and
 run makemigrations / migrate as needed. For example, as a first step, you
 may wish to rename the auth_user table to something in your users app's
 namespace. You can do so by removing db_table from your User model, so it
 looks like this:

 {{{
 class User(AbstractUser):
     pass
 }}}

 You'll also need to create and run a new migration to make this change in
 the database:

 `./manage.py makemigrations --name rename_user_table`
 `./manage.py migrate`

 ----

 Please let me know if this works for you - especially with other
 databases.

 [1] https://www.caktusgroup.com/blog/2019/04/26/how-switch-custom-django-
 user-model-mid-project/ with mo

-- 
Ticket URL: <https://code.djangoproject.com/ticket/25313#comment:24>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/064.826ea1eb9bf0c94108eecc66cdfa6193%40djangoproject.com.

Reply via email to