Building QuerySet of Users from their Profile models

Hi all,

I’ve got what is probably a relatively simple question. I’ve read through the related documentation but I’m still getting stumped.

I think it’s probably just a case of not writing the proper syntax. I would really appreciate any help and advice you can give.

SCENARIO:

• I’ve created a CustomUser and a related Profile model.
• Within the app it is possible for a user to follow another.
• This is handled via a Contact model linked to via a ManyToMany field.

The diagram below shows the relationship between two CustomUser models. Blue lines are the direction I’m interested in i.e. User A to User B.

TRYING TO ACCOMPLISH:

Once those following relationships are created, I’m trying to build a list of followers for a user. For example, on User A’s profile page, it should be possible to view a list of all the user’s that are following them.

PROBLEM:

The problem is my (lack of) knowledge of the Django query syntax means I cannot find a way to traverse the relationship within the views.py file. All I can return is a QuerySet of Profile models:


def user_followers(request, username):
        user = get_object_or_404(get_user_model(), username=username)
        followers = user.profile.followers.all()
        return render(request, 'profile.html', {'user': user, 'followers': followers})

This just returns a list of Profiles.

I’ve tried lots of different variations (e.g. double underscore) to go that extra step and get the corresponding User models, but haven’t been able to find the correct syntax to do this yet.

ALTERNATIVES:

I can think of a couple of potential workarounds for this:

  1. In the views.py file, start from the CustomUser object and then filter the queryset based on whether they are following the currently-viewed user. However this seems excessive, as it would mean for every single user in the database, checking their list of followers.
  2. In the HTML template, update the list syntax to say something along the lines of {% for member.user in members %} however I’d really like to avoid customising my HTML when the logic should be easily accommodated in the view.
  3. Similar to #2, but within views.py iterate over the list of Profiles to get another list of corresponding CustomUsers.

Of all these #3 feels the closest, but again I’m just finding my way in the dark without knowledge of the query syntax. Can anyone help point me in the right direction?

Many thanks in advance,
Tom

Hi,

You can pull the users by filtering on the reverse relationship of the ManyToManyField (see the docs here).

What it would look like for you is something like:

User.objects.filter(profile__following_set__user=user)

This will fetch the users who have a profile that are being followed by the given user. Where as profile.followers.all() gives you access to the profiles that follow a given user.

The usage of following_set depends on the definition of the ManyToManyField and the related_name and/or related_query_name.

Edit:

I now realize this is your alternative option #1.

1 Like

Regarding your concern about option 1 being inefficient, I suspect you’ll be fine. If you have large dataset, go ahead an check it’s performance with an explain analyze (you can use the Django Debug Toolbar to help with this), but with smaller datasets you’ll be fine. And if that’s the case, I believe delivering functionality is more important than future proofing against performance issues that may or may not crop up.

Hi Tim, thanks for your replies, nice to know I’m not going mad that someone else thought of option #1 as well!

As you say, I guess it’s a case of premature optimisation. That was my only concern with option #1 (i.e. that it would require querying the “following” list of every user in the database).

I’ll go with option #1 for now.

However it’s so strange that it’s so easy to just modify the template with a single line and it works fine, whereas it seems almost impossible to do the same thing in views.py. Makes me think there MUST be a way of doing it, because surely the template synax is just shorthand for what can be done in views.py? I’m scratching my head!

profile.html template list:

# create a for loop of the 'followers' list
{% for follower in followers %}
   # for each follower, retrieve the user model and name it 'member'
   {% with member=follower.user %}
      # display the member/user's username
      <h2>{{ member.username }}</h2>
   {% endwith %}
{% endfor %}

As Tim has pointed out, some of the syntax is going to be specific to the precise way in which these models are defined.

So to that end, it might be really helpful at this point if you posted the actual models you’re working with - it’s going to make it easier to provide suggestions using the correct identifiers.

Sorry, yes good point. I was hoping the diagram would cover it but you’re right it doesn’t show things like related_name fields.

Here’s the three models I’m working with. I’ve included all current fields for each model.

CUSTOM USER:

class CustomUser(AbstractUser):

    def __str__(self):
        return self.username

PROFILE:

class Profile(models.Model):
    user = models.OneToOneField(CustomUser, on_delete=models.CASCADE)
    bio = models.TextField(default='', max_length=480)
    location = models.CharField(default='', max_length=240)
    image = models.ImageField(default='default_user_pic.jpg', upload_to='user_profile_pics')
    following = models.ManyToManyField('self', through='Contact', related_name='followers', symmetrical=False)

    def __str__(self):
        return f'{self.user.username} Profile'

CONTACT:

class Contact(models.Model):
    user_from = models.ForeignKey(Profile, related_name='rel_from_set', on_delete=models.CASCADE)
    user_to = models.ForeignKey(Profile, related_name='rel_to_set', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ('-created',)
    
    def __str__(self):
        return f'{self.user_from} follows {self.user_to}'

If you only need fields on the User class, then I still stand by the suggestion of:

user = get_object_or_404(get_user_model(), username=username)
followers = User.objects.filter(profile__followers__user=user)

If not and you’d also need the profile files then I’d recommend using select_related to pull the user fields so you don’t end up with a N+1 issue.

user = get_object_or_404(get_user_model().objects.select_related('profile'), username=username)
followers = user.profile.followers.all().select_related('user')

You’re correct that this would mean your templates would require something like:

{% for follower in followers %}
   # for each follower display the profile's user's username
   <h2>{{ follower.user.username }}</h2>
{% endfor %}