2025, Nov 24 18:02
Как привязать кнопку «добавить в друзья» к автору поста в Django
Как в Django корректно показывать кнопку «подписаться/добавить в друзья» в ленте: считаем состояния по user_id, упрощаем шаблон с elif, убираем циклы.
Отображать кнопку «добавить в друзья» или «подписаться» под каждым постом в ленте Django кажется простой задачей, пока модель данных и логика шаблонов не начинают работать друг против друга. На практике несоответствие идентификаторов, разрозненные циклы в шаблонах и излишне сложный контекст приводят к тому, что кнопка либо не появляется, либо показывает неверное состояние. Ниже — понятный путь, как сделать так, чтобы кнопка корректно отражала отношения между текущим пользователем и автором поста.
Постановка задачи
Есть базовые модели: профиль пользователя со списком друзей ManyToMany, пост с владельцем-пользователем и модель заявок в друзья между пользователями. Представление ленты собирает «зипованный» список профилей и состояний кнопок, а шаблон итерируется по постам, затем по этому предрассчитанному списку, пытаясь решить, какую кнопку показывать.
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 %}
Что на самом деле не так
Здесь сталкиваются две проблемы. Во‑первых, шаблон пытается показать кнопку для автора поста, перебирая отдельный, заранее собранный список состояний профилей. Этот список никак не привязан к автору текущего поста, поэтому прямого соответствия между постом и нужным состоянием нет. Результат — неверные кнопки или вовсе что-то бесполезное.
Во‑вторых, источники связей не совпадают. Посты ссылаются на пользователя через ForeignKey, а дружба хранится в профилях пользователей. Сравнение id профиля с id пользователя всегда будет мимо. Правильно сравнивать id пользователей с id пользователей. Практичный подход — заранее посчитать три множества идентификаторов пользователей: текущие друзья, исходящие ожидающие заявки и входящие ожидающие заявки. Тогда в шаблоне достаточно проверить принадлежность owner_id поста к одному из этих наборов. И ещё одна важная деталь в шаблонах: Django использует elif, а не else if; запись else if приводит к ошибке «неправильный тег шаблона».
Рефакторинг: посчитать один раз — проверять для каждого поста
Решение — собрать в представлении ровно те идентификаторы, которые нужны, и максимально упростить логику шаблона. Ниже множество друзей формируется через связь профиля, а множества ожидающих заявок — из модели ConnectInvite. Затем в шаблоне одна цепочка условий с elif выбирает корректную кнопку для автора каждого поста.
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 %}
Почему это важно
Когда логика соотносится с моделью данных, исчезают тонкие рассинхроны. Посты указывают на пользователей; значит, состояние дружбы тоже должно определяться по идентификаторам пользователей. Предрасчёт состояния в представлении упрощает шаблоны и избавляет от вложенных циклов, в которых легко запутаться. Это также убирает случайную связанность между несвязанными queryset’ами. И наконец, аккуратный синтаксис шаблонов помогает избежать лишних ошибок: в Django поддерживается только elif.
Итоги
Привязывайте состояние кнопки напрямую к автору поста, а не к стороннему списку. Сравнивайте id пользователей с id пользователей, получая их через связь профиля. Готовьте в представлении простые множества для друзей и ожидающих заявок, а в шаблоне выражайте интерфейс одной цепочкой проверок с elif. Если что-то всё ещё выглядит странно, выведите значения в шаблон или добавьте простые принты в код — так вы увидите, какие ветки реально срабатывают. Это сделает ленту предсказуемой, а кнопку заявок в друзья — на своём месте.