Django ORM, seeding users and related objects by OneToOneField

I am designing a django application for educational purposes.

I've come up with creating a fake banking application.

The idea is to have a User<->BankAccount link by a OneToOneField. Similarly, to have User<->UserProfile link by a OneToOneField.

Attached is my models.py:

from django.db import models
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User

import secrets

ACCOUNT_ID_PREFIX = "SBA_"

CURRENCY_CHOICES = [
        ('USD', '($) US Dollars'),
        ('EUR', '(Є) European Dollars'),
        ('ILS', '(₪) New Israeli Shekels'),
]

GENDER_CHOICES = [
    ('MALE', 'Male'),
    ('FEMALE', 'Female')
]

class Currencies(models.Model):
        country = models.CharField(max_length=50)
        code = models.CharField(max_length=6, choices=CURRENCY_CHOICES, unique=True)
        sign = models.CharField(max_length=4)


class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=30, default="John")
    middle_name = models.CharField(max_length=30, default="Damien")
    last_name = models.CharField(max_length=30, default="Doe")

    gender=models.CharField(
        max_length=10,
        choices=GENDER_CHOICES
    )

class BankAccount(models.Model):
    owner = models.OneToOneField(User, on_delete=models.PROTECT)

    # Account ID of format SBA_12345678
    account_id = models.CharField(max_length=16,
        unique=True,
        null=False)

    balance = models.DecimalField(max_digits=20, decimal_places=2)
    currency_code = models.ForeignKey(Currencies, on_delete=models.PROTECT)

    def save(self, *args, **kwargs):
        if self._state.adding:
            acctid = BankAccount.make_new_acct_id()
            if not acctid:
                raise Exception("Failed to create new Account ID (attempts exhausted)")
            print("Saving acctid={}".format(acctid))
            self.account_id = acctid

        super(BankAccount, self).save(*args, **kwargs)

    @staticmethod
    def make_new_acct_id() -> str | None:
        prefix = ACCOUNT_ID_PREFIX
        accid = ""

        attempts_left = 5
        while attempts_left > 0:
            accid = prefix + str(secrets.randbelow(100_000_000)).zfill(8)

            if not BankAccount.objects.filter(account_id=accid).exists():
                # success, we made a unique account id
                return accid

            attempts_left -= 1

        # We failed to make a new accid
        return None

I've created a new empty migration for my seeds 0003_seed_users_and_accounts.py

from django.db import migrations
import random
from django.conf import settings
from django.contrib.auth import get_user_model

seed_data = [
    # username is only for lookup; UserProfile doesn’t store it directly
    {'first_name': 'John', 'middle_name': 'Martin', 'last_name': 'Doe', 'gender': 'MALE', 'username':'john.d', 'password':'jb2025example.cz', 'email':'john.b@example.cz'},
    {'first_name': 'Alice', 'middle_name': 'B.', 'last_name': 'Smith', 'gender': 'FEMALE', 'username': 'alice.b', 'password':'alice.b@example.cz', 'email':'ab2025example.cz'},
    {'first_name': 'Bob', 'middle_name': 'C.', 'last_name': 'Johnson', 'gender': 'MALE', 'username': 'bob.c', 'password':'bob.c@example.cz', 'email':'bc2025example.cz'},
    {'first_name': 'Carol', 'middle_name': 'D.', 'last_name': 'Williams',  'gender': 'FEMALE', 'username': 'carol.d', 'password':'carol.d@example.cz', 'email':'cd2025example.cz'},
    {'first_name': 'David', 'middle_name': 'E.', 'last_name': 'Brown', 'gender': 'MALE', 'username': 'david.e', 'password':'david.e@example.cz', 'email':'de2025example.cz'},
    {'first_name': 'Eve', 'middle_name': 'F.', 'last_name': 'Jones', 'gender': 'FEMALE', 'username': 'eve.f', 'password':'eve.f@example.cz', 'email':'ef2025example.cz'},
]

class Migration(migrations.Migration):

    dependencies = [
        ('main', '0002_seed_initial_data'),
    ]

    def seed_initial_users_and_profiles(apps, schema_editor):
        UserProfile = apps.get_model("main", "UserProfile")
        BankAccount = apps.get_model("main", "BankAccount")
        Currencies = apps.get_model("main", "Currencies")
        User = get_user_model()
        currency = Currencies.objects.get(code="USD")

        for data in seed_data:
            username =  data.pop('username')
            email =     data.pop('email')
            password =  data.pop('password')
            balance = random.randint(900, 100_000_000)
            
            user = User.objects.create(username=username, email=email, password=password)
            # Seed debugging print
            print("\nType of user variable={}\n".format(type(user)))

            UserProfile.objects.create(user=user, **data)
            BankAccount.objects.create(currency_code=currency, owner=user, balance=balance)

    operations = [
        migrations.RunPython(seed_initial_users_and_profiles),
    ]

When I try to run the migration, I'm getting the following error

ValueError: Cannot assign "<User: john.d>": "UserProfile.user" must be a "User" instance.

You might have noticed I added a debug print right before the seed tries to create a new user profile with the user variable.

The output is: Type of user variable=<class 'django.contrib.auth.models.User'>

As I've followed The django doc regarding OneToOneField implementation, I'm quite baffled.

Would love for any assistance on the matter, thank you readers!

Django's migrations do not use the actual models, but historical ones, so "shadow copies" of the existing. This might sometimes be necessary: if you created a model with two fields, and then later add a new one, your data migration was made under the assumption that it still had two fields. So Django's migrations tooling makes small models (without properties, methods and other tooling you have added to these). This allows to write data migrations at the time, that will still work if you have modified the model later on.

You thus should work with:

def seed_initial_users_and_profiles(apps, schema_editor):
    UserProfile = apps.get_model('main', 'UserProfile')
    BankAccount = apps.get_model('main', 'BankAccount')
    Currencies = apps.get_model('main', 'Currencies')
    User = apps.get_model('auth', 'User')
    # …
Вернуться на верх