Django migration cannot assign User instance to user field

Viewed 667

Problem


Recently I created a new migration for a SubscriptionAccount model which replaces an email field with a link to the User model. In this migration I also do a small data migration which fetches the users with the email and sets it to the SubscriptionAccount.user. However, for some reason the migration does not want to assign the user to the newly created user field. Complaining that it cannot assign a User instance ant that it expects a User instance. Which is weird, because that's exactly what I'm doing.

Error


The following error occurs:

Traceback (most recent call last):
  File "./manage.py", line 13, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 364, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.6/site-packages/django/core/management/__init__.py", line 356, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 283, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/base.py", line 330, in execute
    output = self.handle(*args, **options)
  File "/usr/local/lib/python3.6/site-packages/django/core/management/commands/migrate.py", line 204, in handle
    fake_initial=fake_initial,
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 115, in migrate
    state = self._migrate_all_forwards(state, plan, full_plan, fake=fake, fake_initial=fake_initial)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 145, in _migrate_all_forwards
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/executor.py", line 244, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/migration.py", line 126, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/usr/local/lib/python3.6/site-packages/django/db/migrations/operations/special.py", line 193, in database_forwards
    self.code(from_state.apps, schema_editor)
  File "/project/hypernodedb/migrations/0069_link_subscription_account_to_user.py", line 26, in migrate_email_to_user
    subscription_account.user = user
  File "/usr/local/lib/python3.6/site-packages/django/db/models/fields/related_descriptors.py", line 220, in __set__
    self.field.remote_field.model._meta.object_name,
ValueError: Cannot assign "<User: admin@example.com>": "SubscriptionAccount.user" must be a "User" instance.

What have I tried


I have tried casting the user I fetch to an actual User instance, but to no avail. I did a lot of debugging and see there's some difference in modules, seeing that at some point the module of the user gets changed to __fake__. However this is just inside of Django, so I'm not sure what that is about. I've checked all types in the migration and everything seems to be in order, except for the __fake__ module.

I also tried upgrading to the latest django and DRF versions, but this did not work.

The code


def migrate_email_to_user(apps, schema_editor):
    SubscriptionAccount = apps.get_model('hypernodedb', 'SubscriptionAccount')
    User = get_user_model()

    for subscription_account in SubscriptionAccount.objects.all():
        user, _ = User.objects.get_or_create(username=subscription_account.email)
        subscription_account.user = user
        subscription_account.save()


def revert_migrate_email_to_user(apps, schema_editor):
    SubscriptionAccount = apps.get_model('hypernodedb', 'SubscriptionAccount')

    for subscription_account in SubscriptionAccount.objects.all():
        subscription_account.email = subscription_account.user.username
        subscription_account.save()


class Migration(migrations.Migration):

    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('hypernodedb', '0068_change_app_name_validation_error'),
    ]

    operations = [
        migrations.AddField(
            model_name='subscriptionaccount',
            name='user',
            field=models.OneToOneField(null=True, default=None, on_delete=django.db.models.deletion.CASCADE,
                                       to=settings.AUTH_USER_MODEL),
            preserve_default=False,
        ),
        migrations.RunPython(
            code=migrate_email_to_user,
            reverse_code=revert_migrate_email_to_user,
        ),
        migrations.RemoveField(
            model_name='subscriptionaccount',
            name='email',
        ),
    ]

The following is the SubscriptionAccount as it stands when migrating:

class SubscriptionAccount(TimeStampedModel):
    user = models.OneToOneField(get_user_model(), null=True)
    external_id = models.CharField(max_length=255)

    def __str__(self):
        return '{} - {}'.format(self.user.username, self.external_id)

This was the model before the changes:

class SubscriptionAccount(TimeStampedModel):
    # TODO: link this to a user model in our auth system
    email = models.CharField(max_length=255, unique=True)
    external_id = models.CharField(max_length=255)

    def __str__(self):
        return '{} - {}'.format(self.user.username, self.external_id)
1 Answers

Turns out that calling get_user_model() is not correct to use in the migration. Which is, if you think about it, logical. Since you should never import the models directly in migrations. However the migration error threw me off, complaining that it was expecting a User and not a User. Also the fact that using the get_user_model() function is a little different than importing it directly (i.e. what if you use a different User model).

The way to resolve this is to import the User model from apps:

User = apps.get_model('auth', 'User')

After this the migration went fine.

Related