Django База [2023]: Система Лайк / Дизлайк с помощью JavaScript ❤️🔥 #36
В этой статье мы рассмотрим создание системы Лайк / Дизлайк в Django без перезагрузки страницы с помощью JavaScript.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Создание модели рейтинга
Первым делом мы создадим модель рейтинга, для этого в файле models.py приложения blog добавим код модели:
from django.db import models
class Rating(models.Model):
"""
Модель рейтинга: Лайк - Дизлайк
"""
article = models.ForeignKey(to=Article, verbose_name='Статья', on_delete=models.CASCADE, related_name='ratings')
user = models.ForeignKey(to=User, verbose_name='Пользователь', on_delete=models.CASCADE, blank=True, null=True)
value = models.IntegerField(verbose_name='Значение', choices=[(1, 'Нравится'), (-1, 'Не нравится')])
time_create = models.DateTimeField(verbose_name='Время добавления', auto_now_add=True)
ip_address = models.GenericIPAddressField(verbose_name='IP Адрес')
class Meta:
unique_together = ('article', 'ip_address')
ordering = ('-time_create',)
indexes = [models.Index(fields=['-time_create', 'value'])]
verbose_name = 'Рейтинг'
verbose_name_plural = 'Рейтинги'
def __str__(self):
return self.article.title
Этот код определяет модель Rating, которая наследуется от models.Model из Django. Модель содержит следующие поля:
article
- поле внешнего ключа, которое ссылается на модель Article.user
- поле внешнего ключа, которое ссылается на модель User. Оно может быть пустым.value
- поле выбора значений, которое может иметь одно из двух значений: 1 (Нравится) или -1 (Не нравится).time_create
- поле даты и времени, которое устанавливается автоматически при создании объекта.ip_address
- поле IP-адреса.
Модель определяет несколько метаданных для управления ее поведением. unique_together гарантирует уникальность комбинации article и ip_address. ordering указывает порядок сортировки по умолчанию, indexes определяет индекс на поля time_create
и value
. verbose_name и verbose_name_plural определяют отображаемые имена модели в административном интерфейсе.
str метод определен для модели Rating, чтобы при печати объекта модели выводилось название связанной статьи.
Не забываем провести миграции: py manage.py makemigrations
и py manage.py migrate
Подсчет рейтинга у статей/статьи, оптимизация N+1
Теперь нам необходимо написать функцию подсчета суммы рейтинга и добавить рейтинг в prefetch_related
в нашем менеджере, для этого у модели Article добавим следующие изменения:
class Article(models.Model):
"""
Модель постов для сайта
"""
class ArticleManager(models.Manager):
"""
Кастомный менеджер для модели статей
"""
def all(self):
"""
Список статей (SQL запрос с фильтрацией для страницы списка статей)
"""
return self.get_queryset().select_related('author', 'category').prefetch_related('ratings').filter(status='published')
def detail(self):
"""
Детальная статья (SQL запрос с фильтрацией для страницы со статьёй)
"""
return self.get_queryset()\
.select_related('author', 'category')\
.prefetch_related('comments', 'comments__author', 'comments__author__profile', 'tags', 'ratings')\
.filter(status='published')
title = models.CharField(verbose_name='Заголовок', max_length=255)
slug = models.SlugField(verbose_name='Альт.название', max_length=255, blank=True, unique=True)
# Другие поля...
# Другие функции...
def get_sum_rating(self):
return sum([rating.value for rating in self.ratings.all()])
Отлично. Перейдем к представлению.
Создаем представление для работы с рейтингом
Нам необходимо создать представление, которое будет работать с JS, для этого в нашем файле views.py приложения blog добавим следующее представление:
from django.http import JsonResponse
from django.views.generic import View
from .models import Rating
from ..services.utils import get_client_ip
class RatingCreateView(View):
model = Rating
def post(self, request, *args, **kwargs):
article_id = request.POST.get('article_id')
value = int(request.POST.get('value'))
ip_address = get_client_ip(request)
user = request.user if request.user.is_authenticated else None
rating, created = self.model.objects.get_or_create(
article_id=article_id,
ip_address=ip_address,
defaults={'value': value, 'user': user},
)
if not created:
if rating.value == value:
rating.delete()
return JsonResponse({'status': 'deleted', 'rating_sum': rating.article.get_sum_rating()})
else:
rating.value = value
rating.user = user
rating.save()
return JsonResponse({'status': 'updated', 'rating_sum': rating.article.get_sum_rating()})
return JsonResponse({'status': 'created', 'rating_sum': rating.article.get_sum_rating()})
Этот код представляет класс-контроллер, который обрабатывает POST-запросы на создание и изменение модели Рейтинг (Rating). Он использует стандартный метод post()
для обработки запроса.
При получении запроса, представление извлекает данные из запроса, включая идентификатор статьи, значение оценки, IP-адрес клиента (получаем с помощью функции, которую мы создали в этом уроке) и аутентифицированного пользователя (если таковой имеется). Затем используется метод get_or_create()
для получения экземпляра модели Рейтинг (Rating). Если объект уже существует, он либо удаляется, либо обновляется соответствующим образом, а если объект новый, он сохраняется.
Наконец, метод возвращает JsonResponse
, содержащий статус операции ('created', 'updated' или 'deleted') и сумму оценок статьи, полученных с помощью метода get_sum_rating()
, определенного в модели Article.
Определим контроллер в urls.py
Добавим обработку представления в urls.py нашего приложения blog:
from django.urls import path
from .views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, articles_list, ArticleCreateView, \
ArticleUpdateView, ArticleDeleteView, CommentCreateView, ArticleByTagListView, ArticleSearchResultView, RatingCreateView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('articles/', articles_list, name='articles_by_page'),
path('articles/create/', ArticleCreateView.as_view(), name='articles_create'),
path('articles/<str:slug>/update/', ArticleUpdateView.as_view(), name='articles_update'),
path('articles/<str:slug>/delete/', ArticleDeleteView.as_view(), name='articles_delete'),
path('articles/<str:slug>/', ArticleDetailView.as_view(), name='articles_detail'),
path('articles/<int:pk>/comments/create/', CommentCreateView.as_view(), name='comment_create_view'),
path('articles/tags/<str:tag>/', ArticleByTagListView.as_view(), name='articles_by_tags'),
path('category/<str:slug>/', ArticleByCategoryListView.as_view(), name="articles_by_category"),
path('search/', ArticleSearchResultView.as_view(), name='search'),
path('rating/', RatingCreateView.as_view(), name='rating'),
]
Создадим JavaScript для обработки рейтинга у статей/статьи
Ранее в уроках по созданию системы комментариев с помощью JavaScipt мы уже создавали подобные файлы и подключали, ознакомьтесь, если забыли, или пришли в статью не из уроков: комментарии без перезагрузки в Django, в нем мы создаем файл для csrftoken.
Но я напомню, если вы ранее не создавали систему комментариев с JS, то вам необходим следующий код для отправки csrftoken и использования его в JS:
const getCookie = (name) => {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
};
const csrftoken = getCookie("csrftoken");
Этот код определяет функцию getCookie
, которая используется для получения значения cookie
по имени, и объявляет переменную csrftoken
, используя функцию getCookie
для извлечения значения cookie
с именем "csrftoken"
. Этот код может быть использован в других файлах JavaScript, если они ссылаются на этот код, где csrftoken
доступен для использования.
Сам файл backend.js с csrftoken подключаем в самый низ в main.html, перед body:
<script src="{% static 'custom/js/backend.js' %}"></script>
Вернеёмся к созданию рейтинга. Далее я создам файл ratings.js в папке src/custom/js/ со следующим JS кодом системы рейтинга:
const ratingButtons = document.querySelectorAll('.rating-buttons');
ratingButtons.forEach(button => {
button.addEventListener('click', event => {
// Получаем значение рейтинга из data-атрибута кнопки
const value = parseInt(event.target.dataset.value)
const articleId = parseInt(event.target.dataset.article)
const ratingSum = button.querySelector('.rating-sum');
// Создаем объект FormData для отправки данных на сервер
const formData = new FormData();
// Добавляем id статьи, значение кнопки
formData.append('article_id', articleId);
formData.append('value', value);
// Отправляем AJAX-Запрос на сервер
fetch("/rating/", {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"X-Requested-With": "XMLHttpRequest",
},
body: formData
}).then(response => response.json())
.then(data => {
// Обновляем значение на кнопке
ratingSum.textContent = data.rating_sum;
})
.catch(error => console.error(error));
});
});
Данный код устанавливает обработчик кликов на кнопки рейтинга, которые имеют класс 'rating-buttons'. Когда пользователь кликает на одну из кнопок, происходит извлечение значения рейтинга из data-атрибута кнопки, а также id статьи, которую пользователь оценил. Затем создается объект FormData, который содержит эту информацию и отправляется AJAX-запрос на сервер с использованием метода fetch(). В заголовке запроса передается CSRF-токен и заголовок X-Requested-With со значением XMLHttpRequest. После получения ответа сервера, значение суммарного рейтинга обновляется на кнопке с помощью метода textContent.
Добавление кнопок и обработка js на странице списка статей
Далее подключаем ratings.js в articles_list.html и добавляем соответствующие кнопки для лайка, дизлайка и суммы рейтинга:
{% extends 'main.html' %}
{% load static %}
{% block content %}
{% for article in articles %}
<div class="card mb-3">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}">
</div>
<div class="col-8">
<div class="card-body">
<h5 class="card-title"><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h5>
<p class="card-text">{{ article.short_description|safe }}</p>
</hr>
Категория: <a href="{% url 'articles_by_category' article.category.slug %}">{{ article.category.title }}</a>
/ Добавил: {{ article.author.username }}
</div>
</div>
<div class="rating-buttons">
<button class="btn btn-sm btn-primary" data-article="{{ article.id }}" data-value="1">Лайк</button>
<button class="btn btn-sm btn-secondary" data-article="{{ article.id }}" data-value="-1">Дизлайк</button>
<button class="btn btn-sm btn-secondary rating-sum">{{ article.get_sum_rating }}</button>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block script %}
<script src="{% static 'custom/js/ratings.js' %}"></script>
{% endblock%}
Также добавим в шаблон полной статьи новые изменения articles_detail.html:
{% extends 'main.html' %}
{% load mptt_tags static %}
{% block content %}
<div class="card mb-3 border-0 shadow-sm">
<div class="row">
<div class="col-4">
<img src="{{ article.thumbnail.url }}" class="card-img-top" alt="{{ article.title }}" />
</div>
<div class="col-8">
<div class="card-body">
<h5>{{ article.title }}</h5>
<p class="card-text">{{ article.full_description|safe }}</p>
Категория: <a href="{% url 'articles_by_category' article.category.slug %}">{{ article.category.title }}</a> / Добавил: {{ article.author.username }} / <small>{{ article.time_create }}</small>
</div>
</div>
</div>
{% if article.tags.all %}
<div class="card-footer border-0">
Теги записи: {% for tag in article.tags.all %} <a href="{% url 'articles_by_tags' tag.slug %}">{{ tag }}</a>, {% endfor %}
</div>
{% endif %}
<div class="rating-buttons">
<button class="btn btn-sm btn-primary" data-article="{{ article.id }}" data-value="1">Лайк</button>
<button class="btn btn-sm btn-secondary" data-article="{{ article.id }}" data-value="-1">Дизлайк</button>
<button class="btn btn-sm btn-secondary rating-sum">{{ article.get_sum_rating }}</button>
</div>
</div>
<div class="card border-0">
<div class="card-body">
<h5 class="card-title">
Комментарии
</h5>
{% include 'blog/comments/comments_list.html' %}
</div>
</div>
{% endblock %}
{% block script %}
<script src="{% static 'custom/js/ratings.js' %}"></script>
{% endblock%}
{% block sidebar %}
<div class="card mb-2 border-0">
<div class="card-body">
<div class="card-title">
Похожие статьи
</div>
<div class="card-text">
<ul class="similar-articles">
{% for sim_article in similar_articles %}
<li><a href="{{ sim_article.get_absolute_url }}">{{ sim_article.title }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endblock %}