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. Если что-то всё ещё выглядит странно, выведите значения в шаблон или добавьте простые принты в код — так вы увидите, какие ветки реально срабатывают. Это сделает ленту предсказуемой, а кнопку заявок в друзья — на своём месте.