Django ORM, набор пользователей и связанных с ними объектов с помощью OneToOneField

Я разрабатываю приложение django для образовательных целей.

Я придумал создать поддельное банковское приложение.

Идея состоит в том, чтобы иметь ссылку на учетную запись пользователя->BankAccount с помощью OneToOneField. Аналогично, чтобы иметь ссылку User<->UserProfile с помощью OneToOneField.

Прилагаю мой 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

Я создал новую пустую миграцию для своих семян 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),
    ]

Когда я пытаюсь запустить миграцию, я получаю следующую ошибку

ValueError: Не удается назначить "<User: john.d>": "UserProfile.user" должен быть экземпляром "User".

Возможно, вы заметили, что я добавил отладочную печать непосредственно перед тем, как начальная версия попытается создать новый профиль пользователя с переменной user.

Результатом будет: Тип пользовательской переменной=<класс 'django.contrib.auth.models.Пользователь'>

Поскольку я следил за Документом django, касающимся реализации OneToOneField, я совершенно сбит с толку.

Буду рад любой помощи по этому вопросу, спасибо вам, читатели!

Миграции Django используют не фактические модели, а исторические, то есть "теневые копии" существующих. Иногда это может быть необходимо: если вы создали модель с двумя полями, а затем добавили новое, перенос данных был произведен в предположении, что в ней по-прежнему было два поля. Таким образом, инструментарий миграции Django создает небольшие модели (без свойств, методов и других инструментов, которые вы к ним добавили). Это позволяет сразу записывать данные для миграции, которые все равно будут работать, если вы позже измените модель.

Таким образом, вы должны работать с:

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')
    # …
Вернуться на верх