2025, Nov 17 01:00
How to Render the Correct Friend/Follow Button per Post in a Django Feed Using User ID Sets
Fix wrong or missing friend/follow buttons in a Django feed. Map user IDs, precompute friend and invite states in the view, and use elif logic in templates.
Rendering a friend or follow button on every post in a Django feed sounds straightforward until the data model and template logic start working against each other. In practice, mismatched IDs, detached loops in templates, and overly complex context can make the button never appear or show the wrong state. Below is a clear path to make the button reflect the relationship between the current user and the post author.
Problem setup
The core models include a user profile with a ManyToMany list of friends, a post owned by a user, and a friend request model between users. The feed view builds a zipped list of profiles and button states, and the template iterates over posts and then over that precomputed list trying to decide which button to show.
from django.conf import settings
from django.db import models
from django.db.models import Q
from django.shortcuts import render
class UserProfile(models.Model):
person = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
peers = models.ManyToManyField('UserProfile', related_name='peer_of', blank=True)
class FeedItem(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
text = models.TextField(blank=True, null=True)
class ConnectInvite(models.Model):
sender = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='invites_sent')
receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='invites_received')
def stream_view(request):
page_heading = "For You"
feed_rows = FeedItem.objects.all()
profiles_qs = UserProfile.objects.exclude(Q(person=request.user)).order_by('-id')
people_bucket = []
state_flags = []
for prof in profiles_qs:
u = prof.person
people_bucket.append(u)
is_peer = UserProfile.objects.filter(person=request.user, peers__id=prof.id).exists()
state = 'none'
if not is_peer:
state = 'not_friend'
if len(ConnectInvite.objects.filter(sender=request.user).filter(receiver=u)) == 1:
state = 'cancel_request_sent'
if len(ConnectInvite.objects.filter(sender=u).filter(receiver=request.user)) == 1:
state = 'follow_back_request'
state_flags.append(state)
ctx = {
'page_title': page_heading,
'feed_rows': feed_rows,
'profiles_with_state': zip(profiles_qs, state_flags),
'people_bucket': people_bucket,
}
return render(request, 'stream.html', ctx)
{% if feed_rows %}
{% for row in feed_rows %}
{{ row.owner }}
{% if not row == request.user %}
{% for item in profiles_with_state %}
{% if item.1 == 'not_friend' %}
{% elif item.1 == 'cancel_request_sent' %}
{% elif item.1 == 'follow_back_request' %}
{% else %}
{% endif %}
{% endfor %}
{% endif %}
{{ row.text }}
{% endfor %}
{% endif %}
What’s really going wrong
Two issues collide here. First, the template tries to render the button for a post author by iterating over a separate, precomputed list of profile states. That list is not keyed to the post author currently being rendered, so there is no direct mapping from the post to the relevant state. The result is either incorrect buttons or nothing useful at all.
Second, the relationship sources do not match. Posts reference a user via a ForeignKey, while the friends relationship lives on user profiles. Comparing a profile id to a user id will always miss. The right approach is to compare user ids to user ids. A practical way to make this work is to precompute three sets of user ids: current friends, pending outgoing requests, and pending incoming requests. Then the template can simply check membership against the post’s owner_id. One more detail that matters in templates: Django uses elif, not else if, and using else if leads to a malformed template tag error.
Refactor: compute once, check per post
The fix is to gather exactly the ids needed in the view and keep the template logic dead simple. Below, the friends set is collected by fetching user ids through the profile relation, and the pending request sets are collected from the ConnectInvite model. The template then uses a single chain of conditions with elif to pick the correct button per post author.
from django.conf import settings
from django.db import models
from django.shortcuts import render
class UserProfile(models.Model):
person = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
peers = models.ManyToManyField('UserProfile', related_name='peer_of', blank=True)
class FeedItem(models.Model):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True)
text = models.TextField(blank=True, null=True)
class ConnectInvite(models.Model):
sender = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='invites_sent')
receiver = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='invites_received')
def stream_view(request):
friend_user_ids = set(
request.user.userprofile.peers.values_list('person', flat=True)
)
sent_invite_user_ids = set(
ConnectInvite.objects.filter(sender=request.user).values_list('receiver_id', flat=True)
)
incoming_invite_user_ids = set(
ConnectInvite.objects.filter(receiver=request.user).values_list('sender_id', flat=True)
)
ctx = {
'page_title': 'For You',
'feed_rows': FeedItem.objects.all(),
'friend_user_ids': friend_user_ids,
'sent_invite_user_ids': sent_invite_user_ids,
'incoming_invite_user_ids': incoming_invite_user_ids,
}
return render(request, 'stream.html', ctx)
{% for row in feed_rows %}
{{ row.owner }}
{% if row.owner_id in friend_user_ids %}
{% elif row.owner_id in sent_invite_user_ids %}
{% elif row.owner_id in incoming_invite_user_ids %}
{% else %}
{% endif %}
{{ row.text }}
{% endfor %}
Why this matters
Keeping the logic aligned to the data model prevents subtle mismatches. Posts point to users; your friendship state should also be keyed by user ids. Precomputing the state in the view keeps templates lean and avoids nested loops that are hard to reason about. It also removes accidental coupling between unrelated querysets. Finally, clarity in template syntax avoids avoidable failures; elif is the only supported conditional branch form in Django templates.
Wrap-up
Bind button state to the post author directly, not to a side list. Compare user ids to user ids by collecting them through the profile relationship. Prepare simple, membership-based sets for friends and pending requests in the view, then express the UI as a single set of elif checks in the template. If something still feels off, inspect values in the template or add basic print debugging to see what flows through each branch. This keeps the feed predictable and the friend request button exactly where it should be.