Django 4: Лайки/дизлайки в мини-блоге

На предыдущем уроке был реализован функционал комментариев. Теперь улучшим внешний вид формы и вывода комментариев, добавив систему «лайки/дизлайки».

Сложность реализации

Основная сложность — отсутствие авторизации и регистрации пользователей. Без идентификации пользователей, один и тот же клиент может многократно ставить лайки/дизлайки с одного IP-адреса, искажая статистику. Поэтому ограничим возможность ставить лайк/дизлайк с одного IP-адреса до одного раза. Для этого будем использовать IP-адрес клиента.

Модель для хранения лайков

Создадим модель Like для хранения информации о лайках. Она будет содержать IP-адрес пользователя и связь с конкретным постом.

class Like(models.Model):
    """Лайки"""
    ip = models.CharField(max_length=100, verbose_name='IP адрес')
    post = models.ForeignKey('Post', on_delete=models.CASCADE, verbose_name='Публикация')

Модель наследуется от models.Model. Атрибут ip хранит IP-адрес (тип CharField с ограничением длины). post — связь с моделью Post (используется ForeignKey с on_delete=models.CASCADE для каскадного удаления лайков при удалении поста). Модель не будет отображаться в админ-панели, так как просмотр IP-адресов не обязателен в этом проекте.

После создания модели выполним миграции:

python manage.py makemigrations
python manage.py migrate

Получение IP-адреса клиента

Функция get_client_ip получает IP-адрес клиента из запроса. Получение IP-адреса не всегда тривиально, так как может использоваться прокси-сервер. Функция пытается получить реальный IP-адрес из заголовка HTTP_X_FORWARDED_FOR, а в случае неудачи — из заголовка REMOTE_ADDR.

def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

Представления для лайков и дизлайков

Создадим представления LikeView и DislikeView для обработки лайков и дизлайков. Они получают IP-адрес клиента и ID поста. Если лайк от данного IP-адреса для данного поста уже существует, происходит редирект на ту же страницу. В противном случае, лайк добавляется в базу данных, и также происходит редирект. Для дизлайков, если запись существует, она удаляется; иначе, происходит редирект.

class LikeView(View):
    def get(self, request, pk):
        client_ip = get_client_ip(request)
        try:
            like = Like.objects.get(ip=client_ip, post_id=pk)
            return redirect(reverse('post_detail', args=[pk]))
        except Like.DoesNotExist:
            Like.objects.create(ip=client_ip, post_id=pk)
            return redirect(reverse('post_detail', args=[pk]))

class DislikeView(View):
    def get(self, request, pk):
        client_ip = get_client_ip(request)
        try:
            like = Like.objects.get(ip=client_ip, post_id=pk)
            like.delete()
            return redirect(reverse('post_detail', args=[pk]))
        except Like.DoesNotExist:
            return redirect(reverse('post_detail', args=[pk]))

Настройка URL

Добавим URL-паттерны для новых представлений:

path('<int:pk>/like/', LikeView.as_view(), name='add_like'),
path('<int:pk>/dislike/', DislikeView.as_view(), name='del_like'),

Изменение шаблона

В шаблоне post_detail.html добавим блок для отображения количества лайков и кнопок «нравится» и «не нравится»:

<div>
  <br>
  <p>Понравилось: {{ post.likes.count }}</p>
  <br>
  <a href="{% url 'add_like' post.pk %}">Нравится</a>
  <a href="{% url 'del_like' post.pk %}">Не нравится</a>
</div>

Добавление стилей CSS

Для улучшения внешнего вида формы и комментариев добавим стили CSS в файл styles.css. Примеры стилей приведены [в источнике/видео].

В этом уроке реализован функционал лайков/дизлайков с ограничением по одному лайку/дизлайку с одного IP-адреса. Создана модель для хранения лайков, написаны представления для обработки запросов, внесены изменения в шаблон и CSS для улучшения внешнего вида.

Что будем искать? Например,программа