neisor
neisor

Reputation: 432

Django - OneToOneField does not work - RelatedObjectDoesNotExist: User has no userdetail

I am trying to extend the User model with this:

class UserDetail(models.Model):
    user = models.OneToOneField(to=User, on_delete=models.CASCADE, verbose_name="Používateľ")

    def __str__(self):
        return f"{self.user.first_name} {self.user.last_name} ({self.user.username}) / Celková hodnota všetkých objednávok používateľa: {self.total_value_of_users_orders} €"

    @property
    def total_value_of_users_orders(self):
        all_orders = Order.objects.all().filter(user=self.user, status__in=["NW", "WP", "PD", "IP", "ST", "DN"])
        total_value = 0
        for order in all_orders:
            total_value += order.total_value
        return total_value

However, when I try to access the total_value_of_users_orders property, it does not work:

from django.contrib.auth import get_user_model
User = get_user_model()
all_users = User.objects.all()

for u in all_users:
    u.userdetail.total_value_of_users_orders

it shows this exception:

RelatedObjectDoesNotExist: User has no userdetail.

What am I doing wrong? I expect all users to have the total_value_of_users_orders property which either returns 0 (if there are no Orders belonging to the given user) or the exact value (if there are Orders belonging to the given user).

Thank you

Upvotes: 2

Views: 456

Answers (3)

Alireza Jafari
Alireza Jafari

Reputation: 79

It means your user does not have a userdetail related one-to-one object in the database. You can create it when you need it or when you create a user.

There are several ways to handle it:

  1. Creating UserDetail object immediately after creating user. This can be easily done using django signals. (You may need to modify generated migration to create UserDetail for old ones.) It can be done in model manager too. Model manager is less problematic and easier to debug than signals. You can google known problems with signals.
  2. Creating UserDetail object (if doesn't exist) when you want to access it. You can implement it yourself or use django-annoying's AutoOneToOneField. It would be introducing a side effect to a retriever though. It also sort of means leaving your model in an invalid state until the relationship is accessed. Always go for option 1 if the relationship is required.
  3. Checking if user has a userdetail or not (using hasattr) every time you want to access it. Read the documentation. This is the correct way to go if the relationship is nullable (optional, not required). Only, it is ugly. It should have been as simple as if user.detail. So maybe it can be improved.

There are other options that may work much better if you can extend the User model, but it seems you already migrated it, so it won't be that easy.

In the end there is only two ways it can be: one to one relationship is either required or optional. if it is required, one shouldn't exist without the other. so they should be created together. if it is optional, it should be treated as nullable.

Upvotes: 3

Shishir Subedi
Shishir Subedi

Reputation: 629

Use django signal to create UserDetail at the time of User creation so that each user will have its own UserDetail and query can access total_value_of_users_order

from django.db.models.signals import post_save

# ... your models

def create_user_detail(sender, created, instance, **kwargs):
    if created:
        UserDetail.objects.create(user=instance)

# ...
post_save.connect(create_user_detail, sender=User)

Upvotes: 2

lucutzu33
lucutzu33

Reputation: 3710

You need to check for existence first:

if hasattr(u, 'userdetail'):
    print(u.userdetail.total_value_of_users_orders)

Note that the following will still give you an RelatedObjectDoesNotExist error:

if u.userdetail:
    print(u.userdetail.total_value_of_users_orders)

Upvotes: 4

Related Questions