Django База [2023]: Вывод просматриваемых статей за промежуток времени (7 / 1 дней) 💫 #48
Django

Django База [2023]: Вывод просматриваемых статей за промежуток времени (7 / 1 дней) 💫 #48

Razilator

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

Этот урок является расширением предыдущего урока: Django База [2023]: Счетчик уникальных просмотров для статей 👀 #47, поэтому вам необходим код из прошлого урока для реализации вывода популярных статей.

Вспомним, что в модель мы добавили дату сохранения просмотра, а именно viewed_on = models.DateTimeField(auto_now_add=True, verbose_name='Дата просмотра'), с этим полем мы и будем работать.

Создание пользовательского simple тега для вывода популярных статей

Напоминаю, что теги сохраняются в нашем проекте по следующему пути: blog/templatetags/blog_tags.py, в этом уроке мы создавали теги для комментариев и популярных тегов.

Добавим следующий код:

blog/templatetags/blog_tags.py
from datetime import datetime, date, time, timedelta
from django import template
from django.db.models import Count, Q
from django.utils import timezone

from ..models import Article

register = template.Library()

@register.simple_tag
def popular_articles():
    # получаем текущую дату и время в формате datetime
    now = timezone.now()
    # вычисляем дату начала дня (00:00) 7 дней назад
    start_date = now - timedelta(days=7)
    # вычисляем дату начала текущего дня (00:00)
    today_start = timezone.make_aware(datetime.combine(date.today(), time.min))
    # получаем все статьи и количество их просмотров за последние 7 дней
    articles = Article.objects.annotate(
        total_view_count=Count('views', filter=Q(views__viewed_on__gte=start_date)),
        today_view_count=Count('views', filter=Q(views__viewed_on__gte=today_start))
    ).prefetch_related('views')
    # сортируем статьи по количеству просмотров в порядке убывания, сначала по просмотрам за сегодня, затем за все время
    popular_articles = articles.order_by('-total_view_count', '-today_view_count')[:10]
    return popular_articles

Данный код является Django-шаблон тегом. Он выводит список 10 самых популярных статей за последние 7 дней, отсортированных по количеству просмотров за сегодняшний день и за все время.

Объяснение кода:

  • Строки 1-5: импорт необходимых модулей и модели Article.
  • Строка 7: регистрация шаблонного тега в Django.
  • Строки 9-10: получение текущей даты и вычисление даты начала дня 7 дней назад.
  • Строка 12: вычисление даты начала текущего дня.
  • Строка 14: получение всех статей и количества их просмотров за последние 7 дней. Аннотации total_view_count и today_view_count вычисляют общее количество просмотров за 7 дней и количество просмотров за сегодняшний день, соответственно.
  • Строка 15: запрос к связанным объектам - для улучшения производительности используется метод prefetch_related().
  • Строки 17-20: сортировка статей по количеству просмотров, сначала по просмотрам за все время (total_view_count), затем по просмотрам за сегодня (today_view_count). Отбираются первые 10 статей.
  • Строка 22: возвращение списка популярных статей.

(Небязательно) Счетчик просмотров за сегодня

Важно: прибавляет SQL запросы. Если хотите выводить, то лучше кэшировать HTML фрагмент.

Давайте добавим к модели Статей счетчик просмотров за сегодня, чтобы мы знали сколько у нас просмотров всего, а сколько статью посмотрели сегодня:

blog/models.py
from django.db import models
from datetime import date

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()
    
    def get_today_view_count(self):
        """
        Возвращает количество просмотров для данной статьи за сегодняшний день
        """
        today = date.today()
        return self.views.filter(viewed_on__date=today).count()

Первый метод get_view_count() возвращает общее количество просмотров данной статьи, то есть количество объектов в связанной модели ViewCount через related_name='views', которые относятся к данной статье через внешний ключ.

Второй метод get_today_view_count возвращает количество просмотров данной статьи за текущий день. Он фильтрует связанные объекты ViewCount, чтобы получить только те, которые были просмотрены сегодня. Это достигается через фильтр viewed_on__date=today, который выбирает только те записи в связанной модели ViewCount, где поле viewed_on равно сегодняшней дате, определенной с помощью date.today(). Затем он возвращает количество отфильтрованных записей.

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

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

В прошлом уроке мы создали следующий миксин:

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

class ViewCountMixin:
    """
    Миксин для увеличения счетчика просмотров статьи
    """
    def get_object(self):
        # получаем статью по заданному slug
        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

Мы можем его обновить, чтобы этот миксин сохранял записи на человека каждый день. Миксин выше сохраняет запись просмотра на один ip адрес лишь один раз за всё время. С одной стороны это меньше мусора. А с другой стороны, если сайт молодой, то почему бы и нет?

blog/mixins.py
from .models import ViewCount
from modules.services.utils import get_client_ip
from datetime import timedelta
from django.utils import timezone


class ViewCountMixin:
    """
    Миксин для увеличения счетчика просмотров статьи
    """
    def get_object(self):
        # получаем статью по заданному slug
        obj = super().get_object()
        # получаем IP-адрес пользователя и ключ сессии
        ip_address = get_client_ip(self.request)
        # получаем текущую дату и время
        now = timezone.now()
        # вычитаем 7 дней из текущей даты
        week_ago = now - timedelta(days=1)
        # проверяем, есть ли записи за последние 7 дней для данного пользователя и данной статьи
        # если нет, то создаем новую запись
        ViewCount.objects.get_or_create(article=obj, ip_address=ip_address, viewed_on__gte=week_ago)
        return obj

Вывод популярных статей в sidebar

Далее добавим вывод популяных статей в sidebar:

templates/sidebar.html
{% load mptt_tags blog_tags %}
<div class="card mb-2">
	<div class="card-body">
		<h5 class="card-title">Категории</h5>
		{% full_tree_for_model blog.Category as categories %}
		<div class="card-text">
			<ul>
				{% recursetree categories %}
				<li>
					<a href="{{ node.get_absolute_url }}">{{ node.title }}</a>
				</li>
				{% if not node.is_leaf_node %}
				<ul>
					{% endif %} {{children}} {% if not node.is_leaf_node %}
				</ul>
				{% endif %} {% endrecursetree %}
			</ul>
		</div>
	</div>
</div>
<div class="card mb-2">
	<div class="card-body">
		<h5 class="card-title">Популярные теги</h5>
		<div class="card-text">
			<ul>
				{% popular_tags as tag_list %} 
        		{% for tag in tag_list %}
				<li><a href="{% url 'articles_by_tags' tag.slug %}">{{ tag.name }}</a> ({{ tag.num_times }})</li>
				{% empty %}
				<li>Популярных тегов не найдено.</li>
				{% endfor %}
			</ul>
		</div>
	</div>
</div>

<div class="card mb-2">
	<div class="card-body">
		<h5 class="card-title">Популярные статьи за 7 дней</h5>
		<div class="card-text">
			<ul>
				{% popular_articles as articles_list %} 
        		{% for article in articles_list %}
				<li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a> ({{ article.get_view_count }}) +({{ article.get_today_view_count}})</li>
				{% empty %}
				<li>Популярных статей не найдено.</li>
				{% endfor %}
			</ul>
		</div>
	</div>
</div>

{% show_latest_comments count=5 %}

Проблема N+1

Из-за метода get_today_view_count() мы получаем больше запросов
Из-за метода get_today_view_count() мы получаем больше запросов

К сожалению, метод prefetch_related() тут уже не спасает. Даже если оптимизируем, то всё равно запросы останутся. Одним из решений является кэширование шаблона:

templates/sidebar.html
{% load blog_tags cache %}

{% cache 300 sidebar %}
<div class="card mb-2">
	<div class="card-body">
		<h5 class="card-title">Популярные статьи за 7 дней</h5>
		<div class="card-text">
			<ul>
				{% popular_articles as articles_list %} 
        		{% for article in articles_list %}
				<li><a href="{{ article.get_absolute_url }}">{{ article.title }}</a> ({{ article.get_view_count }}) +({{ article.get_today_view_count}})</li>
				{% empty %}
				<li>Популярных статей не найдено.</li>
				{% endfor %}
			</ul>
		</div>
	</div>
</div>
{% endcache %}

С кэшированием мы разбирались в одном из предыдущих уроков.

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

Ну и ещё одно решение просто не использовать метод get_today_view_count() для вывода количества просмотров за сегодня, поэтому я его и пометил, как необязательно.

Лишние запросы в кэше
Лишние запросы в кэше

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

Список популярных статей на сайте, где () это всего просмотров, где +() - просмотров за день
Список популярных статей на сайте, где () это всего просмотров, где +() - просмотров за день
Список статей в админке, вы можете посчитать даты, чтобы убедиться, что статистика корректная
Список статей в админке, вы можете посчитать даты, чтобы убедиться, что статистика корректная
;