Evgenii Legotckoi
Evgenii Legotckoi13 ноября 2016 г. 1:04

Django - Урок 014. Вывод списка популярных статей на Django

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

Первое, что было решено сделать - это вывод списка популярных статей. Первый вариант популярных статей был основан на общем счётчике просмотров и в итоге выводил статьи с самым большим количество просмотров. Это в целом плохой вариант, поскольку в ТОПе в итоге окажутся статьи просто набравшие самое большое количество просмотров за всё время.

Следовательно нужно было что-то менять. В результате был внедрен вывод популярных статей за последние 7 дней в самом простом варианте. То есть, добавлена была таблица в которую вносится количество просмотров статей по дням. Конечно, точность подсчётов при большой нагрузке может очень сильно варьироваться, но пока посещаемость не достигла 5000 - 10000 уникальных посетителей в сутки - это не настолько важно.

А теперь давайте разберём пример того, как сделать такой список популярных статей средствами Django .


Модели статей и статистики

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

class Article(models.Model):
    class Meta:
        db_table = "article"

    title = models.CharField('Название статьи', max_length=200)

    def __str__(self):
        return self.title

А для модели статистики просмотров по статьям код будет следующий.

class ArticleStatistic(models.Model):
    class Meta:
        db_table = "ArticleStatistic"

    article = models.ForeignKey(Article)                    # внешний ключ на статью
    date = models.DateField('Дата', default=timezone.now)   # дата
    views = models.IntegerField('Просмотры', default=0)     # количество просмотров в эту дату

    def __str__(self):
        return self.article.title


class ArticleStatisticAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'date', 'views')  # отображаемые поля в админке
    search_fields = ('__str__', )                # поле, по которому производится поиск

В данном коде сделано две модели:

  1. Сама модель для сбора статистики
  2. Модель для отображения данных в админке.

Эти модели будут написаны в файле models.py.

Не забываем также зарегистрировать эти модели в админке в файле admin.py.

from django.contrib import admin

from .models import Article, ArticleStatistic, ArticleStatisticAdmin

admin.site.register(Article)
admin.site.register(ArticleStatistic, ArticleStatisticAdmin)

После чего не забываем сделать миграцию базы данных.

python manage.db makemigrations
python manage.db migrate

Представление для отображения страницы

После того, как у нас добавлены модели, осталось добавить вывод популярных статей и сделать подсчёт статистики при запросе статьи с сервера.

from django.views import View
from django.shortcuts import render_to_response, get_object_or_404
from django.utils import timezone
from django.db.models import Sum

# Для работы с моделями статей у меня используется модуль knowledge
from knowledge.models import Article, ArticleStatistic  


# тогда как для отображения статей используется модуль post
# Это было сделано для того, чтобы URL статей был следующего вида
#    /post/42/ - где 42 - это id статьи в базе данных
class EArticleView(View):
    template_name = 'knowledge/article.html'    # Шаблон статьи

    def get(self, request, *args, **kwargs):
        article = get_object_or_404(Article, id=self.kwargs['article_id'])    # Забираем статью из базы данных
        context = {}

        # Далее забираем объект сегодняшней статистики или создаём новый, если требуется
        obj, created = ArticleStatistic.objects.get_or_create(
            defaults={
                "article": article,
                "date": timezone.now()
            },
            # При этом определяем, забор объекта статистики или его создание
            # по двум полям: дата и внешний ключ на статью
            date=timezone.now(), article=article    
        )
        obj.views += 1    # инкрементируем счётчик просмотров и обновляем поле в базе данных
        obj.save(update_fields=['views'])

        # А теперь забираем список 5 последний самых популярных статей за неделю
        popular = ArticleStatistic.objects.filter(
            # отфильтровываем записи за последние 7 дней
            date__range=[timezone.now() - timezone.timedelta(7), timezone.now()]
        ).values(
            # Забираем интересующие нас поля, а именно id и заголовок
            # К сожалению забрать объект по внешнему ключу в данном случае не получится
            # Только конкретные поля из объекта
            'article_id', 'article__title'
        ).annotate(
            # Суммируем записи по просмотрам
            # Всё суммируется корректно с соответствием по запрашиваемым полям объектов 
            views=Sum('views')
        ).order_by(
            # отсортируем записи по убыванию
            '-views')[:5]  # Заберём последние пять записей

        context['popular_list'] = popular # Отправим в контекст список статей

        return render_to_response(template_name=self.template_name, context=context)

Отображение в шаблоне

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

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

{% if popular_list %}{% load bootstrap3 %}
<ul class="list-group">
<li class="list-group-item active"><strong>Популярные публикации за неделю</strong></li>
{% for pop_article in popular_list %}
    <li class="list-group-item">
    <a href="{% url 'post:article' pop_article.article_id %}">{{ pop_article.article__title }}</a>
    </li>
{% endfor %}
</ul>
{% endif %}

Чтобы не было недомолвок, приведу шаблон url для запроса страниц с сайта:

url(r'^(?P<article_id>[0-9]+)/$', views.EArticleView.as_view(), name='article'),

Итог

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

Для Django рекомендую VDS-сервера хостера Timeweb .

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

R
  • 5 января 2017 г. 2:20

Добрый день! В Django я новичок. По Вашей инструкции у меня получилось вывести популярные статьи, но они видны только в статье. Как вывести их на главную страницу? Заранее спасибо за ответ!

Evgenii Legotckoi
  • 5 января 2017 г. 7:50

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

R
  • 5 января 2017 г. 8:41

Спасибо Вам большое!!!

ИМ
  • 30 января 2018 г. 18:24
Здравствуйте Евгений! Чему может быть причиной эта ошибка:
Reverse for 'post' with arguments '(1,)' not found. 1 pattern(s) tried: ['main\\/(?P<section>[^/]+)\\/(?P<post_id>[^/]+)\\/$']
Мой урл, и вьюха.
app_name = 'main'

urlpatterns = [
    path('', views.Index, name='index'),
    path('<section>/', views.SectionView.as_view(), name='section'),
    path('<section>/<post_id>/', views.PostView.as_view(), name='post')
]

class PostView(View):
    template_name = 'main/post.html'    # Шаблон статьи
 
    def get(self, request, *args, **kwargs):
        post = get_object_or_404(Post, id=self.kwargs['post_id'])
        context = {}
        obj, created = PostStatistic.objects.get_or_create(defaults={"post": post,"date": timezone.now()}, date=timezone.now(), post=post)
        obj.views += 1
        obj.save(update_fields=['views'])
 
        popular = PostStatistic.objects.filter(
            date__range=[timezone.now() - timezone.timedelta(7), timezone.now()]
        ).values(
            'post_id', 'post__title'
        ).annotate(
            views=Sum('views')
        ).order_by(
            '-views')[:2]

        context['post_id'] = post
        context['popular_list'] = popular
        context['username'] = auth.get_user(request).username
 
        return render_to_response(template_name=self.template_name, context=context)
Ну и шаблон до кучи:

{% if popular_list %}
<ul class="list-group">
<li class="list-group-item active"><strong>Популярные публикации за неделю</strong></li>
{% for pop_post in popular_list %}
    <li class="list-group-item">
    <a href="{% url 'main:post' pop_post.post_id %}">{{ pop_post.post__title }}</a>
    </li>
{% endfor %}
</ul>
{% endif %}
Очень надеюсь на вашу помощь.
Evgenii Legotckoi
  • 31 января 2018 г. 3:49

Добрый день!

Во-первых перепишите urlpatterns так

urlpatterns = [
    path('', views.Index, name='index'),
    path('<section>/<post_id>/', views.PostView.as_view(), name='post'),
    path('<section>/', views.SectionView.as_view(), name='section')
]
Маршрутизатор url в Django иногда тупит из-за последовательности объявления шаблонов. Поэтому лучше шаблоны url прописывать в алфавитном порядке и больший шаблон ставить первым.

Во-вторых вы используете id, а там нужно тогда указать, что это у вас целочисленное значение, по умолчанию обрабатывается как строка. Значит результирующая запись будет такой.
urlpatterns = [
    path('', views.Index, name='index'),
    path('<section>/<int:post_id>/', views.PostView.as_view(), name='post'),
    path('<section>/', views.SectionView.as_view(), name='section')
]
ИМ
  • 31 января 2018 г. 4:38

К сожалению это не изменило ситуацию. Убираю ссылку на статью из шаблона и все работает.  Вероятно не правильно сформирована ссылка в шаблоне.

Evgenii Legotckoi
  • 31 января 2018 г. 4:45

Да. действительно, на это я не обратил внимания. Напишите либо так

<a href="{% url 'main:post' pop_post.id %}">{{ pop_post.post__title }}</a>
либо так
<a href="{% url 'main:post' pop_post.pk %}">{{ pop_post.post__title }}</a>
ИМ
  • 31 января 2018 г. 5:37

И этот вариант к сожалению не рабочий =(

Evgenii Legotckoi
  • 31 января 2018 г. 6:04

Извиняюсь, неправильно написал последний ответ. В рамках данной статьи это не правильно, у вас же во вьюшке формируется объект у которого в post_id должен содержаться его PrimaryKey.

 
Вообще можете показать модель PostStatistic? Я явно что-то упускаю здесь.
ИМ
  • 31 января 2018 г. 6:22

Такая же как и у вас вроди бы.


class PostStatistic(models.Model):
    class Meta:
        db_table = "PostStatistic"
 
    post = models.ForeignKey(Post, on_delete=models.CASCADE) 
    date = models.DateField('Дата', default=timezone.now)   
    views = models.IntegerField('Просмотры', default=0)
 
    def __str__(self):
        return self.post.title
Evgenii Legotckoi
  • 31 января 2018 г. 7:03

мда... глупо как-то получилось. У вас же в шаблоне два аргумента задано.

Один аргумент - это либо id раздела, либо его имя (вомзожно тоже самое slug поле, а вторым параметром идёт id самого поста).

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

Что-то типо такого
<a href="{% url 'main:post' pop_post.post__section pop_post.post_id %}">{{ pop_post.post__title }}</a>

Думаю, что вам нужно немного поправить ещё вот этот метод
popular = PostStatistic.objects.filter(
    date__range=[timezone.now() - timezone.timedelta(7), timezone.now()]
).values(
    'post_id', 'post__title', 'post__section'
).annotate(
    views=Sum('views')
).order_by(
    '-views')[:2]
ИМ
  • 31 января 2018 г. 10:11

Если делать таким образом, то вьюха отдает id категории а не slug. Попробую пост сделать отдельным приложением. Во всяком случае большое спасибо за помощь.

Evgenii Legotckoi
  • 31 января 2018 г. 10:20

А. Так вы slug использовали? Тогда там надо брать поле slug из section в запросе.

Но реально, сделайте post лучше отдельным приложение.
Я по началу делал на сайте статьи с зависимостью от секции, но потом понял, что это будет ошибкой. Дело в том, что если у вас поменяется раздел у статьи или поста, то тогда пост будет располагаться по другому url, а старый будет отдавать ошибку 404. Это плохо для индексации сайта. Поэтому я вовремя опомнился и сделал url поста независимым от раздела.
ИМ
  • 31 января 2018 г. 10:22

Вот и я пришел к тому же пути. Так вроди бы логично и красиво выглядит.

Владислав Меленчук
  • 10 июня 2021 г. 12:44

Я как понял, этот метод создает статистику каждый день (на каждый день), не удаляя старые данные за день и выводит все данные за сегодняшние просмотры у всех статей?

Да, именно такой и была задумка

спасибо

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
AD

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50баллов,
  • Очки рейтинга-4
m
  • molni99
  • 26 октября 2024 г. 11:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
m
  • molni99
  • 26 октября 2024 г. 11:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20баллов,
  • Очки рейтинга-10
Последние комментарии
ИМ
Игорь Максимов22 ноября 2024 г. 22:51
Django - Урок 017. Кастомизированная страница авторизации на Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi1 ноября 2024 г. 0:37
Django - Урок 064. Как написать расширение для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 октября 2024 г. 18:19
Читалка fb3-файлов на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов5 октября 2024 г. 17:51
Django - Урок 064. Как написать расширение для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55 июля 2024 г. 21:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Сейчас обсуждают на форуме
m
moogo22 ноября 2024 г. 18:17
Mosquito Spray System Effective Mosquito Systems for Backyard | Eco-Friendly Misting Control Device & Repellent Spray - Moogo ; Upgrade your backyard with our mosquito-repellent device! Our misters conce…
Evgenii Legotckoi
Evgenii Legotckoi25 июня 2024 г. 1:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 ноября 2024 г. 17:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject4 июня 2022 г. 13:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…

Следите за нами в социальных сетях