Django База [2023]: Вывод просматриваемых статей за промежуток времени (7 / 1 дней) 💫 #48
В этой статье мы рассмотрим, как вывести список популярных статей за последние 7 дней и за день в Django на основе счетчика просмотров из предыдущей статьи.
Если вы хотите выразить благодарность автору сайта, статей и курса по Django, вы можете сделать это по ссылке ниже:
Этот урок является расширением предыдущего урока: Django База [2023]: Счетчик уникальных просмотров для статей 👀 #47, поэтому вам необходим код из прошлого урока для реализации вывода популярных статей.
Вспомним, что в модель мы добавили дату сохранения просмотра, а именно viewed_on = models.DateTimeField(auto_now_add=True, verbose_name='Дата просмотра')
, с этим полем мы и будем работать.
Создание пользовательского simple тега для вывода популярных статей
Напоминаю, что теги сохраняются в нашем проекте по следующему пути: 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 фрагмент.
Давайте добавим к модели Статей счетчик просмотров за сегодня, чтобы мы знали сколько у нас просмотров всего, а сколько статью посмотрели сегодня:
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()
. Затем он возвращает количество отфильтрованных записей.
(Необязательно) Обновление миксина, чтобы записи просмотров сохранялись уникально, но на каждый день, а не на один раз.
Лично я отказался от этой идеи, чтобы не засорять базу данных. Все таки сами подумайте, на самом деле это большое количество записей. С ростом посетителей количество записей на статью от пользователей возрастет.
В прошлом уроке мы создали следующий миксин:
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 адрес лишь один раз за всё время. С одной стороны это меньше мусора. А с другой стороны, если сайт молодой, то почему бы и нет?
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:
{% 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
К сожалению, метод prefetch_related()
тут уже не спасает. Даже если оптимизируем, то всё равно запросы останутся. Одним из решений является кэширование шаблона:
{% 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()
для вывода количества просмотров за сегодня, поэтому я его и пометил, как необязательно.