Разбивка на страницы в Django

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

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

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

Пример проекта можно найти в django-pagination-example репозитории на GitHub.

Цели

К концу этой статьи вы сможете:

  1. Объясните, что такое разбивка на страницы и почему вы, возможно, захотите ее использовать.
  2. Работайте с Paginator классом Django и Page объектами.
  3. Реализуйте разбивку на страницы в Django с помощью представлений на основе функций и классов.

Конструкции Django

При реализации разбивки на страницы в Django вместо того, чтобы изобретать логику, необходимую для разбивки на страницы, вы будете работать со следующими конструкциями:

  1. Разбиение на страницы - разбивает набор запросов Django или список на блоки из Page объектов.
  2. Страница - содержит фактические данные в разбивке на страницы вместе с метаданными разбивки на страницы.

Давайте рассмотрим несколько кратких примеров.

Разбиение на страницы

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 принимает четыре параметра:

  1. object_list - любой объект с методом count() или __len__(), например, список, кортеж или набор запросов
  2. per_page - максимальное количество элементов для размещения на странице
  3. orphans ( необязательно) - используется для предотвращения того, чтобы на последней странице было очень мало элементов, по умолчанию используется 0
  4. allow_empty_first_page ( необязательно) - как следует из названия, вы можете выдать ошибку EmtpyPage, если вы запретите первой странице быть пустой, установив в качестве аргумента значение False, по умолчанию - True

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

Класс Paginator имеет следующие атрибуты:

  1. count - общее количество объектов
  2. num_pages - общее количество страниц
  3. 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 имеет несколько атрибутов и методов, которые можно использовать при создании вашего шаблона:

  1. number - показывает номер страницы для данной страницы
  2. paginator - отображает связанный Paginator объект
  3. has_next() - возвращает True, если есть следующая страница
  4. has_previous() - - возвращает True, если есть предыдущая страница
  5. next_page_number() - возвращает номер следующей страницы
  6. 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})

Здесь мы:

  1. Определил переменную page_num из URL-адреса.
  2. Создал экземпляр класса Paginator, передав ему необходимые параметры, набор запросов employees и количество сотрудников, которые должны быть указаны на каждой странице.
  3. Сгенерировал объект страницы с именем 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

Это первый вариант, реализующий пользовательский интерфейс разбивки на страницы.

Pagination UI - first flavor

Итак, в этом примере у нас есть ссылки "Предыдущая" и "Следующая", по которым конечный пользователь может переходить со страницы на страницу.

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 UI - second flavor

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 UI - third flavor

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 заканчивается. Вот основные выводы, которые следует запомнить:

  1. Реализовать разбиение на страницы в Django довольно просто благодаря вспомогательным классам Paginator и Page, предоставляемым в комплекте поставки.
  2. Как только представление создано, вы просто передаете обратно объект страницы с разбитыми на страницы данными для использования в шаблоне.
Вернуться на верх