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)