Фильтрация и разбиение на страницы с Django

Если вы хотите создать страницу со списком, позволяющую фильтровать и пагинацию, вам нужно заставить несколько отдельных вещей работать вместе. Django предоставляет некоторые инструменты для пагинации, но документация не говорит нам, как заставить это работать с чем-то еще. Аналогично, django_filter позволяет относительно легко добавлять фильтры к представлению, но не говорит вам, как добавить пагинацию (или другие вещи), не нарушая фильтрацию.

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

Фильтры

Давайте начнем с обзора фильтрации с примера того, как можно создать подкласс ListView для добавления фильтрации. Чтобы фильтровать так, как вы хотите, вам нужно создать подкласс FilterSet и установите filterset_class для этого класса. (См. эту ссылку, чтобы узнать, как написать набор фильтров.)

class FilteredListView(ListView):
    filterset_class = None

    def get_queryset(self):
        # Получите набор запросов, как обычно. Например:
        queryset = super().get_queryset()
        # Затем используйте параметры запроса и набор запросов,
        # чтобы создать экземпляр набора фильтров и сохранить его как атрибут.
        self.filterset = self.filterset_class(self.request.GET, queryset=queryset)
        # Return the filtered queryset
        return self.filterset.qs.distinct()

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # Передайте набор фильтров в шаблон — он обеспечивает форму.
        context['filterset'] = self.filterset
        return context

Вот пример того, как вы можете создать конкретное представление, чтобы использовать его:

class BookListView(FilteredListView):
    filterset_class = BookFilterset

А вот часть шаблона, которая использует форму, созданную набором фильтров, чтобы позволить пользователю управлять фильтрацией.

<h1>Books</h1>
  <form action="" method="get">
    {{ filterset.form.as_p }}
    <input type="submit" />
  </form>

<ul>
    {% for object in object_list %}
        <li>{{ object }}</li>
    {% endfor %}
</ul>

filterset.form - это форма, которая управляет фильтрацией, поэтому мы просто отобразим ее, как захотим, и добавим способ отправки.

Это все, что вам нужно для создания простого фильтрованного представления.

Значения по умолчанию для фильтров

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

Для этого я переопределяю __init__ в моем наборе фильтров и добавляю значения по умолчанию к передаваемым данным:

class BookFilterSet(django_filters.FilterSet):
    def __init__(self, data, *args, **kwargs):
        data = data.copy()
        data.setdefault('format', 'paperback')
        data.setdefault('order', '-added')
        super().__init__(data, *args, **kwargs)

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

Пагинация

Теперь давайте рассмотрим пагинацию в Django.

В ListView в Django есть встроенная поддержка пагинации, которую достаточно легко включить:

class BookListView(FilteredListView):
    paginate_by = 50

После того, как для paginate_by задано количество элементов, которые вы хотите разместить на странице, object_list будет содержать только элементы на текущей странице, а в контекст:

paginator
A Paginator object
page_obj
A Page object
is_paginated
True if there are pages

Нам нужно обновить шаблон, чтобы пользователь мог управлять страницами.

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

{% if is_paginated %}
Page {{ page_obj.number }} of {{ paginator.num_pages }}
{% endif %}

Чтобы сообщить представлению, какую страницу отображать, мы хотим добавить параметр запроса с именем page, значением которого является номер страницы. В простейшем случае мы можем просто сделать ссылку с ?page=N, например:

<a href="?page=2">Goto page 2</a>

Вы можете использовать объекты page_obj и paginator для создания полного набора ссылок на страницы, но сначала нужно решить одну проблему.

Сочетание фильтрации и пагинации

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

Таким образом, если вы находитесь на странице https://example.com/objectlist/?type=paperback, а затем переходите по ссылке страницы, вы окажетесь на странице < tt>https://example.com/objectlist/?page=3, когда вы хотели оказаться на https://example.com/objectlist/?type=paperback&page=3.

Было бы неплохо, если бы Django помог создать ссылки, задающие один параметр запроса без потери существующих, но я нашел хороший пример шаблонного тега на StackOverflow и немного модифицировал его в этот пользовательский шаблонный тег, который помогает в этом:

# <app>/templatetags/my_tags.py
from django import template

register = template.Library()


@register.simple_tag(takes_context=True)
def param_replace(context, **kwargs):
    """
    Return encoded URL parameters that are the same as the current
    request's parameters, only with the specified GET parameters added or changed.

    It also removes any empty parameters to keep things neat,
    so you can remove a parm by setting it to ``""``.

    For example, if you're on the page ``/things/?with_frosting=true&page=5``,
    then

    <a href="/things/?{% param_replace page=3 %}">Page 3</a>

    would expand to

    <a href="/things/?with_frosting=true&page=3">Page 3</a>

    Based on
    https://stackoverflow.com/questions/22734695/next-and-before-links-for-a-django-paginated-query/22735278#22735278
    """
    d = context['request'].GET.copy()
    for k, v in kwargs.items():
        d[k] = v
    for k in [k for k, v in d.items() if not v]:
        del d[k]
    return d.urlencode()

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

{% load my_tags %}

{% if is_paginated %}
  {% if page_obj.has_previous %}
    <a href="?{% param_replace page=1 %}">First</a>
    {% if page_obj.previous_page_number != 1 %}
      <a href="?{% param_replace page=page_obj.previous_page_number %}">Previous</a>
    {% endif %}
  {% endif %}

  Page {{ page_obj.number }} of {{ paginator.num_pages }}

  {% if page_obj.has_next %}
    {% if page_obj.next_page_number != paginator.num_pages %}
      <a href="?{% param_replace page=page_obj.next_page_number %}">Next</a>
    {% endif %}
    <a href="?{% param_replace page=paginator.num_pages %}">Last</a>
  {% endif %}

  <p>Objects {{ page_obj.start_index }}—{{ page_obj.end_index }}</p>
{% endif %}

Теперь, если вы находитесь на странице типа https://example.com/objectlist/?type=paperback&page=3, ссылки будут выглядеть как ?type=paperback&page=2, ?type=paperback&page=4 и т. д.

Полезные ссылки

Я не пробовал, но если вам нужно что-то более сложное для создания таких ссылок, django-qurl-templatetag может быть стоит посмотреть.

Вернуться на верх