Django База [2023]: Счетчик уникальных просмотров для статей 👀 #47
Django

Django База [2023]: Счетчик уникальных просмотров для статей 👀 #47

Razilator

Веб-сайты часто отслеживают количество просмотров своих статей, чтобы измерять популярность контента и принимать решения о его дальнейшем развитии. В этой статье мы рассмотрим, как можно создать уникальный счетчик просмотров статей с помощью модели и миксина в Django.

Модель для счетчика уникальных просмотров в Django

Первым шагом в создании счетчика просмотров статей является создание модели, которая будет хранить информацию о каждом просмотре статьи. Модель выглядит следующим образом:

modules/blog/models.py
from django.db import models


class ViewCount(models.Model):
    """
    Модель просмотров для статей
    """
    article = models.ForeignKey('Article', on_delete=models.CASCADE, related_name='views')
    ip_address = models.GenericIPAddressField(verbose_name='IP адрес')
    viewed_on = models.DateTimeField(auto_now_add=True, verbose_name='Дата просмотра')

    class Meta:
        ordering = ('-viewed_on',)
        indexes = [models.Index(fields=['-viewed_on'])]
        verbose_name = 'Просмотр'
        verbose_name_plural = 'Просмотры'
    
    def __str__(self):
        return self.article.title

В этом коде мы создаем модель ViewCount с тремя полями: article, ip_address и viewed_on.

  • article - это внешний ключ, связывающий просмотр с соответствующей статьей.
  • ip_address - это поле для хранения IP-адреса пользователя, который просмотрел статью.
  • viewed_on - это поле для хранения даты и времени просмотра статьи.

Мы также определяем два дополнительных параметра для нашей модели: Meta и str(). Параметр Meta содержит информацию о сортировке и индексировании модели, а также о ее имени и множественном числе для отображения в административном интерфейсе. Параметр str() определяет строковое представление объекта модели, которое будет отображаться в административном интерфейсе.

Не забываем про создание миграций: py manage.py makemigrations и затем py manage.py migrate.

Регистрация ViewCount в админ-панеле

Не забываем для просмотра данных добавить модель ViewCount в админ-панель, следующим образом:

modules/blog/admin.py
from django.contrib import admin

from .models import ViewCount

@admin.register(ViewCount)
class ViewCountAdmin(admin.ModelAdmin):
    pass

По необходимости вы можете настроить необходимые поля для отображения.

Создание миксина для добавления просмотров к статье

Следующим шагом является создание миксина для создания объекта модели ViewCount, при просмотре детальной статьи.

Миксин выглядит следующим образом, сам файл с миксинами я создал в modules/blog/mixins.py:

modules/blog/mixins.py
from .models import ViewCount
from modules.services.utils import get_client_ip


class ViewCountMixin:
    """
    Миксин для увеличения счетчика просмотров статьи
    """
    def get_object(self):
        # получаем статью из метода родительского класса
        obj = super().get_object()
        # получаем IP-адрес пользователя
        ip_address = get_client_ip(self.request)
        # получаем или создаем запись о просмотре статьи для данного пользователя
        ViewCount.objects.get_or_create(article=obj, ip_address=ip_address)
        return obj

Данный код представляет миксин ViewCountMixin, который является частью реализации уникального счетчика просмотров статей в Django.

Метод get_object(self) переопределен в миксине ViewCountMixin и используется для получения объекта статьи, на которую происходит просмотр. В данном коде метод get_object получает объект статьи из родительского класса, используя метод super().get_object().

Далее, используя модель ViewCount, мы получаем или создаем запись о просмотре статьи для данного пользователя, используя метод get_or_create(). В этом методе мы передаем объект статьи и IP-адрес пользователя, чтобы создать новую запись о просмотре, если она еще не существует. Если же запись уже существует, мы ее получаем.

Наконец, метод get_object возвращает объект статьи. При этом счетчик просмотров для данной статьи будет увеличен только в том случае, если просматривающий пользователь является уникальным. Если пользователь уже просматривал данную статью, то запись о просмотре не будет создана повторно, и счетчик просмотров не увеличится.

В коде мспользуя функцию get_client_ip, получаем IP-адрес пользователя, который просматривает статью. Данную функцию мы добавляли в этом уроке.

Но я напомню, что функция у нас находится в папке модуля services, в файле utils.py. Выглядит функция следующим образом:

modules/services/utils.py
def get_client_ip(request):
    """
    Получение IP адреса
    """
    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

Наследуем данный Mixin в представление (View)

Далее нам необходимо добавить данный миксин в представление, чтобы подсчеты начали работать при просмотре полной статьи. Для этого мы добавим миксин в представление ArticleDetailView в файле views.py:

modules/blog/views.py
from django.views.generic import DetailView

from .models import Article
from .mixins import ViewCountMixin

class ArticleDetailView(ViewCountMixin, DetailView):
    model = Article
    template_name = 'blog/articles_detail.html'
    context_object_name = 'article'
    queryset = model.objects.detail()

    def get_similar_articles(self, obj):
        article_tags_ids = obj.tags.values_list('id', flat=True)
        similar_articles = Article.objects.filter(tags__in=article_tags_ids).exclude(id=obj.id)
        similar_articles = similar_articles.annotate(related_tags=Count('tags')).order_by('-related_tags')
        similar_articles_list = list(similar_articles.all())
        random.shuffle(similar_articles_list)
        return similar_articles_list[:6]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = self.object.title
        context['form'] = CommentCreateForm
        context['similar_articles'] = self.get_similar_articles(self.object)
        return context

Добавление счетчика просмотров статьи

Далее мы должны добавить метод подсчета просмотров в модель Article, для этого мы добавим следующий фрагмент кода:

modules/blog/models.py
class Article(models.Model):
    """
    Модель постов для сайта
    """

    title = models.CharField(verbose_name='Заголовок', max_length=255)
    slug = models.SlugField(verbose_name='Альт.название', max_length=255, blank=True, unique=True)
    
    # Другие поля...
    
    # Другие методы...

    def get_view_count(self):
        """
        Возвращает количество просмотров для данной статьи
        """
        return self.views.count()

Таким образом, этот метод выведет количество просмотров к просматриваемой статье, осталось лишь добавить метод в шаблон.

Добавление счетчика просмотров в шаблон

Добавляем следующее поле в HTML разметку нашего шаблона:

templates/blog/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 }}  / Просмотры: {{ article.get_view_count }}
                  </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%}

Отлично. Также нам нужно решить проблему N+1 характера.

Убираем N+1 в менеджере для счетчика просмотров

Для того, чтобы избавиться от нежелательных запросов в базу данных, нам необходимо добавить views в наш кастомный менеджер статей, а именно в метод all().

modules/blog/models.py
class Article(models.Model):
    """
    Модель постов для сайта
    """

    class ArticleManager(models.Manager):
        """
        Кастомный менеджер для модели статей
        """

        def all(self):
            """
            Список статей (SQL запрос с фильтрацией для страницы списка статей)
            """
            return self.get_queryset().select_related('author', 'category').prefetch_related('ratings', 'views').filter(status='published')

        def detail(self):
            """
            Детальная статья (SQL запрос с фильтрацией для страницы со статьёй)
            """
            return self.get_queryset()\
                .select_related('author', 'category')\
                .prefetch_related('comments', 'comments__author', 'comments__author__profile', 'tags')\
                .filter(status='published')

    STATUS_OPTIONS = (
        ('published', 'Опубликовано'),
        ('draft', 'Черновик')
    )

    title = models.CharField(verbose_name='Заголовок', max_length=255)
    slug = models.SlugField(verbose_name='Альт.название', max_length=255, blank=True, unique=True)
    
    # Другие поля...
    
    # Другие методы...

    def get_view_count(self):
        """
        Возвращает количество просмотров для данной статьи
        """
        return self.views.count()

Проверяем работу просмотров на сайте

Перейдя по каждой статье, я получил лишь по одному просмотру, так как я смотрел с одного IP адреса
Перейдя по каждой статье, я получил лишь по одному просмотру, так как я смотрел с одного IP адреса
Дубликаты запросов не копятся, а просто объединяются в один запрос
Дубликаты запросов не копятся, а просто объединяются в один запрос
Как выглядят просмотры в админ-панеле
Как выглядят просмотры в админ-панеле
;