Разбивка на страницы в Django
Разбивка на страницы - это процесс разбивки больших массивов данных по нескольким отдельным веб-страницам. Вместо того чтобы отправлять все данные пользователю, вы можете определить количество отдельных записей, которые вы хотите отобразить на странице, а затем отправить обратно данные, соответствующие странице, запрошенной пользователем.
Преимущество использования такого метода заключается в том, что он улучшает работу пользователя, особенно при наличии тысяч записей, которые необходимо извлечь. Реализовать разбиение на страницы в Django довольно просто, поскольку Django предоставляет класс Paginator, который вы можете использовать для группировки содержимого на разных страницах.
Разбивка на страницы может быть различной в зависимости от того, как она настроена разработчиком. Тем не менее, в этой статье мы рассмотрим, как включить разбивку на страницы с представлениями на основе функций и классов, используя три различных варианта пользовательского интерфейса.
Пример проекта можно найти в django-pagination-example репозитории на GitHub.
Цели
К концу этой статьи вы сможете:
- Объясните, что такое разбивка на страницы и почему вы, возможно, захотите ее использовать.
- Работайте с
Paginator
классом Django иPage
объектами. - Реализуйте разбивку на страницы в Django с помощью представлений на основе функций и классов.
Конструкции Django
При реализации разбивки на страницы в Django вместо того, чтобы изобретать логику, необходимую для разбивки на страницы, вы будете работать со следующими конструкциями:
- Разбиение на страницы - разбивает набор запросов Django или список на блоки из
Page
объектов. - Страница - содержит фактические данные в разбивке на страницы вместе с метаданными разбивки на страницы.
Давайте рассмотрим несколько кратких примеров.
Разбиение на страницы
from django.contrib.auth.models import User
for num in range(43):
User.objects.create(username=f"{num}")
Здесь мы создали 43 пользовательских объекта.
Далее мы импортируем класс Paginator
и создадим новый экземпляр:
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
print(paginator.num_pages) # => 5
Класс Paginator
принимает четыре параметра:
object_list
- любой объект с методомcount()
или__len__()
, например, список, кортеж или набор запросовper_page
- максимальное количество элементов для размещения на страницеorphans
( необязательно) - используется для предотвращения того, чтобы на последней странице было очень мало элементов, по умолчанию используется0
allow_empty_first_page
( необязательно) - как следует из названия, вы можете выдать ошибкуEmtpyPage
, если вы запретите первой странице быть пустой, установив в качестве аргумента значениеFalse
, по умолчанию -True
Итак, в приведенном выше примере мы разделили пользователей на страницы (или блоки) по десять штук. На первых четырех страницах будет десять пользователей, а на последней странице - трое.
Класс Paginator
имеет следующие атрибуты:
count
- общее количество объектовnum_pages
- общее количество страницpage_range
- итератор диапазона номеров страниц
Для получения согласованных результатов с разбивкой по страницам следует упорядочить набор запросов или модель.
Если вы предпочитаете, чтобы на последней странице было не только три пользователя, вы можете использовать аргумент сироты, например, чтобы добавить последних трех пользователей на предыдущую страницу:
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10, orphans=3)
print(paginator.num_pages) # => 4
Таким образом, когда количество оставшихся объектов на последней странице будет меньше или равно значению orphans
, эти объекты будут добавлены на предыдущую страницу.
Страница
После того, как набор запросов Django был разбит на Page
объектов. Затем мы можем использовать метод page()
для доступа к данным для каждой страницы, передав ему номер страницы:
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.page(1)
print(page_obj) # => <Page 1 of 5>
Здесь page_obj
дает нам объект page, который представляет первую страницу результатов. Затем это можно использовать в ваших шаблонах.
Обратите внимание, что мы не создавали экземпляр
Page
в буквальном смысле. Вместо этого мы получили экземпляр из класса Paginator.
Что произойдет, если страница не существует?
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.page(99)
Вы должны увидеть:
raise EmptyPage(_('That page contains no results'))
django.core.paginator.EmptyPage: That page contains no results
Таким образом, хорошей идеей будет перехватить EmptyPage
исключение следующим образом:
from django.contrib.auth.models import User
from django.core.paginator import EmptyPage, Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
try:
page_obj = paginator.page(99)
except EmptyPage:
# Do something
pass
Возможно, вам также захочется перехватить исключение PageNotAnInteger
.
Подробнее об этом читайте в разделе Исключения документации Paginator.
Тем не менее, если вы предпочитаете не обрабатывать исключения EmptyPage
или PageNotAnInteger
явно, вы можете использовать метод get_page() вместо page()
:
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.get_page(99)
print(page_obj) # => <Page 5 of 5>
Таким образом, даже если число 99
выходит за пределы диапазона, будет возвращена последняя страница.
Кроме того, если на странице указан неверный номер, get_page()
по умолчанию будет возвращена первая страница:
from django.contrib.auth.models import User
from django.core.paginator import Paginator
users = User.objects.all()
paginator = Paginator(users, 10)
page_obj = paginator.get_page('foo')
print(page_obj) # => <Page 1 of 5>
Таким образом, в зависимости от ваших предпочтений можно использовать оба метода - page()
или get_page()
. В примерах, приведенных в этой статье, будут использоваться page()
.
Объект Page
имеет несколько атрибутов и методов, которые можно использовать при создании вашего шаблона:
number
- показывает номер страницы для данной страницыpaginator
- отображает связанныйPaginator
объектhas_next()
- возвращаетTrue
, если есть следующая страницаhas_previous()
- - возвращаетTrue
, если есть предыдущая страницаnext_page_number()
- возвращает номер следующей страницыprevious_page_number()
- возвращает номер предыдущей страницы
Представления на основе функций
Далее давайте рассмотрим, как работать с разбивкой на страницы в представлениях на основе функций:
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from . models import Employee
def index(request):
object_list = Employee.objects.all()
page_num = request.GET.get('page', 1)
paginator = Paginator(object_list, 6) # 6 employees per page
try:
page_obj = paginator.page(page_num)
except PageNotAnInteger:
# if page is not an integer, deliver the first page
page_obj = paginator.page(1)
except EmptyPage:
# if the page is out of range, deliver the last page
page_obj = paginator.page(paginator.num_pages)
return render(request, 'index.html', {'page_obj': page_obj})
Здесь мы:
- Определил переменную
page_num
из URL-адреса. - Создал экземпляр класса
Paginator
, передав ему необходимые параметры, набор запросовemployees
и количество сотрудников, которые должны быть указаны на каждой странице. - Сгенерировал объект страницы с именем
page_obj
, который содержит разбитые на страницы данные о сотрудниках, а также метаданные для перехода на предыдущую и следующую страницы.
https://github.com/testdrivenio/django-pagination-example/blob/main/employees/views.py
Представления на основе классов
Пример реализации разбивки на страницы в представлении на основе классов:
from django.views.generic import ListView
from . models import Employee
class Index(ListView):
model = Employee
context_object_name = 'employees'
paginate_by = 6
template_name = 'index.html'
https://github.com/testdrivenio/django-pagination-example/blob/main/employees/views.py
Шаблоны
Работа с разбивкой по страницам в шаблоне становится все интереснее, поскольку существует несколько различных реализаций. В этой статье мы рассмотрим три различные реализации, каждая из которых показывает разные способы перехода к предыдущей и следующей страницам.
Вы можете найти код для каждого примера в папке templates на странице django-разбивка на страницы-пример репозитория на GitHub.
Вкус 1
Это первый вариант, реализующий пользовательский интерфейс разбивки на страницы.
Итак, в этом примере у нас есть ссылки "Предыдущая" и "Следующая", по которым конечный пользователь может переходить со страницы на страницу.
index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css">
<title>Pagination in Django</title>
</head>
<body>
<div class="container">
<h1 class="text-center">List of Employees</h1>
<hr>
<ul class="list-group list-group-flush">
{% for employee in page_obj %}
<li class="list-group-item">{{ employee }}</li>
{% endfor %}
</ul>
<br><hr>
{% include "pagination.html" %}
</div>
</body>
</html>
pagination.html:
<div>
<span>
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
<span>
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
</span>
</div>
Имейте в виду, что шаблон pagination.html можно повторно использовать во многих шаблонах.
Вкус 2
pagination.html:
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% else %}
<a>Previous</a>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<a href="#">{{ i }} </a>
{% else %}
<a href="?page={{ i }}">{{ i }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% else %}
<a>Next</a>
{% endif %}
В этом варианте представлены все номера страниц в пользовательском интерфейсе, что упрощает переход к различным страницам.
Вкус 3
pagination.html:
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">« Previous page</a>
{% if page_obj.number > 3 %}
<a href="?page=1">1</a>
{% if page_obj.number > 4 %}
<span>...</span>
{% endif %}
{% endif %}
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<a href="?page={{ num }}">{{ num }}</a>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<a href="?page={{ num }}">{{ num }}</a>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
{% if page_obj.number < page_obj.paginator.num_pages|add:'-3' %}
<span>...</span>
<a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
{% elif page_obj.number < page_obj.paginator.num_pages|add:'-2' %}
<a href="?page={{ page_obj.paginator.num_pages }}">{{ page_obj.paginator.num_pages }}</a>
{% endif %}
<a href="?page={{ page_obj.next_page_number }}">Next Page »</a>
{% endif %}
Если у вас большое количество страниц, возможно, вы захотите ознакомиться с этим третьим и последним вариантом.
Заключение
На этом статья о реализации разбивки на страницы в Django заканчивается. Вот основные выводы, которые следует запомнить:
- Реализовать разбиение на страницы в Django довольно просто благодаря вспомогательным классам
Paginator
иPage
, предоставляемым в комплекте поставки. - Как только представление создано, вы просто передаете обратно объект страницы с разбитыми на страницы данными для использования в шаблоне.