How do I access objects (user info) across Django models? (Foreign Key, Beginner)

Question:

I have a Profile model and a Post model in my Django project:

class Profile(models.Model):
    """ User profile data """
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='profile')
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    id_user = models.IntegerField()
    bio = models.TextField(max_length=280, blank=True)

    def __str__(self):
        return str(self.user)


class Post(models.Model):
    """ Model for user-created posts """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='post')
    post_text = models.TextField()
    created_at = models.DateTimeField(default=datetime.now)

    def __str__(self):
        return self.user

I want to be able to display a user’s first_name and last_name on their posts (which are shown on index.html with other all user posts), along with their username.

{% for post in posts %}
<div class="row d-flex justify-content-between">
    <h5 class="col-12 col-md-6">{{ first_name here }} {{ last_name here }}</h5>
    <h5 class="col-12 col-md-6">(@{{ post.user }})</h5>
</div>

I can display the username just fine (post.user above) because it’s in the Post model. The first_name and last_name are in the Profile model, however, and I just can’t figure out a way to access them.

View:


@login_required(login_url='signin')
def index(request):
    """ View to render index """
    user_object = User.objects.get(username=request.user.username)

    user_profile = Profile.objects.get(user=user_object)

    posts = Post.objects.all()

    context = {'user_profile': user_profile, 'posts': posts, }

    return render(request, 'index.html', context)

As well as getting all Post objects, my view contains a reference to the currently logged in user (user_object) and their profile (user_profile) for customising things in the navbar and links to the logged in user’s profile.

Based on this, my logic is that I should be doing adding something like this to my view:

post_author = Post.user
author_profile = Profile.objects.get(user=post_author)

so that I can reference something like this in the {% for post in posts %} loop:

{{ post_author.profile.first_name }} {{ author.profile.last_name }}

But of course, that doesn’t work:

TypeError at /
Field ‘id’ expected a number but got <django.db.models.fields.related_descriptors.ForwardManyToOneDescriptor object at 0x7f176924f160>.
Request Method: GET
Request URL: http://localhost:8000/
Django Version: 3.2.13
Exception Type: TypeError
Exception Value:
Field ‘id’ expected a number but got <django.db.models.fields.related_descriptors.ForwardManyToOneDescriptor object at 0x7f176924f160>.

I was following a tutorial but it doesn’t cover this, and I’d really like to figure out where I’m going wrong and gain a better understanding of things. If I could get this right in principle, I should also be able to add the avatar to each post as well. On the other hand, maybe it’s just not possible to do this the way I’ve set my project up?

Asked By: Eoin

||

Answers:

You are on the right track!

The ‘key’ here is how things follow along relationships like foreignkey(many to one).

Currently you’re in your template, cycling through your posts. You have the info you need, it’s just a matter of accessing it. In a template, you can follow along a foreign key just with the dot. So we want to go from post -> fk -> user -> reverse fk -> profile -> field

(we’ll worry about the reverse fk later, for now just know we can use the related name to traverse it)

So this should work:

{% for post in posts %}
<div class="row d-flex justify-content-between">
    <h5 class="col-12 col-md-6">{{ post.user.profile.0.first_name }} {{ post.user.profile.0.first_name }}</h5>
    <h5 class="col-12 col-md-6">(@{{ post.user }})</h5>
</div>

It’s not ideal though…

We’ve used profile.0 because, with your current definitions profile->user is a foreignkey, or many-to-one relationship, so you are allowing multiple profiles for the same user. Thus post.user.profiles will return a set (even if it’s a set of one) and we only want the first object in the set, hence .0. If you only want one profile per user (and generally you do), change your

class Profile(models.Model):
    """ User profile data """
    user = models.ForeignKey(
            User, on_delete=models.CASCADE, related_name='profile')

to

user = models.OneToOneField(
        User, on_delete=models.CASCADE, related_name='profile')

then remigrate your database. Now you can use

<h5 class="col-12 col-md-6">{{ post.user.profile.first_name }} {{ post.user.profile.first_name }}</h5>

The next problem is you will make a database call EVERY TIME you get poster info as you cycle through it, which will be slow if you’ve got lots of posts. This is possibly a little further than you’ve gotten yet, but it’s good to bear in mind for the future:

You can make your post cycle more efficient by preloading all the required info in a single DB call in your view.

If you’ve done as I suggested and made the profile a one-to-one relationship, then we can also use select_related to follow the poster to their profile and grab the profile info when we get the post (there’s a foreignkey from post to user and a one_to_one from user to profile, so select_related should work, otherwise we need prefetch_related to follow a foreignkey)

  Post.objects.all().select_related('user__profile')

You access it in exactly the same way in your template – it’s just grabbed more efficiently.

Answered By: SamSparx