Сейчас на главную страницу проекта выводится ограниченное число записей. Даже если на сайте опубликовано несколько тысяч постов — пользователь сможет прочесть только десять из них: во view-функции главной страницы стоит ограничение на вывод постов.
Чтобы получить доступ к большому списку материалов сайта (будь то посты в блоге или товары в интернет-магазине) — список разбивают постранично, выводя на каждую страницу определённое количество записей. Для перехода между страницами добавляют специальные ссылки.
Список ссылок для постраничного перехода — это стандартный элемент интерфейса:
Написать постраничное разбиение материалов можно самостоятельно:
- при постраничном делении передать в GET-запросе переменную
page
, значением которой будет номер запрошенной страницы, например/group/cats?page=8
(восьмая страница с постами сообщества, посвящённого котикам); - во view-функции проверить, есть ли переменная
page
в GET-параметрах; - если в параметрах нет переменной
page
— отдавать первую страницу, с постами с первого по десятый; - если переменная
page
есть:- посчитать количество записей в базе,
- через
OFFSET
иLIMIT
получить нужный диапазон записей и отобразить их на странице. Для/group/cats?page=8
нужно будет получить записи с 71-ой по 80-ую включительно, если разбиение будет по десять постов на страницу.
Но писать такой код для каждого списка элементов было бы нерационально, и в Django решили эту задачу: стандартный модуль Paginator раскладывает списки объектов по отдельным страницам и создаёт множество дополнительных инструментов для управления такими страницами. Для названия этой системы в русском языке используют кальку с английского: «паджинатор».
Объект класса Paginator получает на вход список объектов, разбивает его на отдельные страницы и позволяет обратиться к ним индивидуально или получить полный список получившихся страниц.
Задача разработчика — создать объект Paginator()
, передать в него список объектов и число элементов, которое требуется выводить на одну страницу. Всё остальное модуль сделает сам, остаётся только обращаться к свойствам и методам созданного объекта:
>>> from django.core.paginator import Paginator
# Создаём тестовый список объектов
>>> items = ['Антон Чехов', 'Владимир Набоков', 'Лев Толстой', 'Марина Цветаева']
# Cоздаём объект Paginator(object_list, per_page),
# он принимает на вход список объектов
# и число объектов, которое должно отображаться на одной странице.
# Передаём список items и задаём постраничное деление: по два объекта на страницу
>>> p = Paginator(items, 2)
# Свойство count показывает, сколько объектов в последовательности
>>> p.count
4
# Свойство num_pages показывает сколько страниц получится из списка
# num_pages рассчитывается как len(items)/per_page
>>> p.num_pages
2
# Получаем объект с элементами для первой страницы
>>> page1 = p.get_page(1)
>>> page1
<Page 1 of 2>
# Получаем элементы для отображения на первой странице
>>> page1.object_list
['Антон Чехов', 'Владимир Набоков']
# Проверяем, есть ли страницы после текущей
# и надо ли отображать кнопку "Следующая страница"
>>> page1.has_next()
True
# Проверяем, есть ли страницы перед текущей
# и надо ли отображать кнопку "Предыдущая страница"
>>> page1.has_previous()
False
>>> page2 = p.get_page(2)
>>> page2.object_list
['Лев Толстой', 'Марина Цветаева']
>>> page2.has_next()
False
>>> page2.has_previous()
True
# Чтобы отобразить список с номерами доступных страниц,
# получим значение свойства page_range:
# в нём хранятся данные типа range
>>> type(p.page_range)
<class 'range'>
# выведем в консоль линейку с перечнем страниц
>>> for n in p.page_range:
... print(f'<{n}> ', end='')
...
<1> <2>
При запросе данных из базы тоже возвращается список объектов, и работать с ним можно точно так же, как со списком писателей из приведённого примера.
>>> from posts.models import Post
>>> posts = Paginator(Post.objects.order_by('-pub_date'), 2)
# В переменную post_page передадим объект второй страницы паджинатора
>>> post_page = posts.get_page(2)
>>> post_page.object_list
<QuerySet [<Post: 36>, <Post: 35>]>
Свойства объекта страницы post_page
, их тип данных и значения в приведённом примере:
- post_page — значение
<Page 2 of 19>
, типdjango.core.paginator.Page
- post_page.has_next() — значение
True
, типbool
- post_page.has_previous() — значение
True
, типbool
- post_page.has_other_pages() — значение
True
, типbool
- post_page.next_page_number() — значение
3
, типint
- post_page.previous_page_number() — значение
1
, типint
- post_page.start_index() — номер первого элемента на текущей странице. Отсчёт идёт от начала списка, начиная с 1. Значение:
3
, тип:int
- post_page.end_index() — номер последнего элемента на текущей странице. Отсчёт идёт от начала списка, начиная с 1. Значение:
4
, тип:int
Методы start_index()
и end_index()
будут полезны, если нужно нумеровать элементы, выведенные на страницу.
Паджинатор в Yatube
Обновите view-функцию главной страницы: пусть она отдаёт по десять постов на страницу. Они будут передаваться в переменной page
.
Для перехода по страницам паджинатора к обычному URL будет добавляться GET-параметр page
с указанием нужной страницы: page=n
, где n
— это номер страницы. Например, URL второй страницы паджинатора для главной страницы проекта будет таким: http://127.0.0.1:8000/?page=2
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Post
def index(request):
post_list = Post.objects.all().order_by('-pub_date')
# Если порядок сортировки определен в классе Meta модели,
# запрос будет выглядить так:
# post_list = Post.objects.all()
# Показывать по 10 записей на странице.
paginator = Paginator(post_list, 10)
# Из URL извлекаем номер запрошенной страницы - это значение параметра page
page_number = request.GET.get('page')
# Получаем набор записей для страницы с запрошенным номером
page_obj = paginator.get_page(page_number)
# Отдаем в словаре контекста
context = {
'page_obj': page_obj,
}
return render(request, 'posts/index.html', context)
Настройка HTML-шаблонов
Измените шаблон index.html: список постов будет передаваться в элементе с ключом page
.
{% extends "base.html" %}
{% block title %}Последние обновления на сайте{% endblock %}
{% block header %}Последние обновления на сайте{% endblock %}
{% block content %}
{% for post in page_obj %}
...Тут вывод списка записей...
{% endfor %}
{% endblock %}
Помимо списка объектов заданной страницы переменная page_obj
содержит объект paginator
, в котором доступны все методы свойства объекта Paginator. В результате к этому объекту можно обратиться, запросив его методы, свойства или атрибуты, это пригодится при работе с шаблоном:
page_obj.paginator.per_page
— сколько записей на страницу? Десять.page_obj.paginator.num_pages
— сколько страниц получилось? Четыре (если вы ничего не добавили к постам Льва Толстого).
Остальные свойства и методы можно найти в официальной документации модуля Paginator.
Теперь нужно написать HTML-код для навигации по страницам. Паджинатор понадобится на нескольких страницах проекта, и чтобы не дублировать код навигации в разных шаблонах — напишите виджет в отдельном файле, и его можно будет включать через include
туда, где он необходим.
Логика проекта предполагает, что паджинатор будет применяться только в приложении posts, так что файл с паджинатором можно разместить вместе с шаблонами приложения posts.
Если использование паджинации предполагается в нескольких приложения — лучше вынести этот файл в папку /includes на уровень шаблонов проекта.
В директории templates/posts/includes создайте файл paginator.html и добавьте в него такой код:
{# templates/posts/includes/paginator.html #}
{# Отрисовываем навигацию паджинатора только если
все посты не помещаются на первую страницу #}
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation" class="my-5">
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">Первая</a></li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">
Предыдущая
</a>
</li>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">
Следующая
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">
Последняя
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
Осталось встроить виджет паджинатора в код шаблона главной страницы:
{% extends "base.html" %}
{% block title %}Последние обновления на сайте{% endblock %}
{% block content %}
{% for post in page_obj %}
...Тут вывод списка записей...
{% endfor %}
{% include 'posts/includes/paginator.html' %}
{% endblock %}
Точно так же паджинатор можно добавить в любой другой шаблон, где может потребоваться постраничное разделение контента. Разумеется, view-функции тоже надо будет изменить.