Шаг 07. Фильтрация, поиск, сортировка и пагинация
⚡ Кратко: что делаем на этом шаге
Цель: Добавить к задачам фильтрацию по status, priority, project, assignee; полнотекстовый поиск; сортировку; пагинацию с настройкой глобально в settings.py.
- Установка:
pip install django-filter - Файлы:
apps/tasks/filters.py(новый),apps/tasks/views.py(обновление),config/settings.py(пагинация + DjangoFilterBackend) - Проверка:
GET /api/tasks/?status=todo&ordering=-created_at&page=1
🎯 Цель этапа
Реальные API не возвращают всё подряд — пользователь ищет задачи по статусу, проекту, исполнителю, сортирует по дате, листает страницы. На этом шаге мы добавляем три уровня управления выборкой:
- Фильтрация — точные совпадения по полям (django-filter + FilterSet)
- Поиск — полнотекстовый поиск в title/description (DRF SearchFilter)
- Сортировка — по любому разрешённому полю (DRF OrderingFilter)
- Пагинация — страничная навигация (PageNumberPagination глобально)
После этого шага у нас будет
GET /api/tasks/?status=todo&priority=high— фильтрацияGET /api/tasks/?search=авторизация— полнотекстовый поискGET /api/tasks/?ordering=-created_at— сортировка по убываниюGET /api/tasks/?page=2&page_size=10— постраничная навигация
📄 Затрагиваемые файлы
| Файл | Действие | Описание |
|---|---|---|
apps/tasks/filters.py | Создать | TaskFilter — FilterSet с полями status, priority, project, assignee, due_date |
apps/tasks/views.py | Обновить | Подключить filter_backends, filterset_class, search_fields, ordering_fields |
config/settings.py | Обновить | DEFAULT_FILTER_BACKENDS, DEFAULT_PAGINATION_CLASS, PAGE_SIZE |
🔨 Шаги
1. Установка django-filter
pip install django-filter
# Зафиксировать в requirements.txt
pip freeze | grep django-filter >> requirements.txt
2. Регистрация django_filters в INSTALLED_APPS
INSTALLED_APPS = [
# ... Django apps ...
"rest_framework",
"django_filters", # ← добавить
"apps.users",
"apps.projects",
"apps.tasks",
]
3. Глобальные настройки DRF: фильтры и пагинация
REST_FRAMEWORK = {
# ... существующие настройки ...
# Фильтры — применяются ко всем ViewSet-ам по умолчанию
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
# Пагинация — PageNumberPagination со страницами по 20 записей
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
}
DEFAULT_FILTER_BACKENDS в настройках, мы применяем фильтры
ко всем ViewSet-ам проекта. Можно переопределить в конкретном ViewSet через атрибут
filter_backends.
4. Создание FilterSet для задач
# apps/tasks/filters.py
import django_filters
from .models import Task
class TaskFilter(django_filters.FilterSet):
"""
Фильтр задач через django-filter.
Поддерживаемые параметры:
?status=todo — точное совпадение по статусу
?priority=high — точное совпадение по приоритету
?project=1 — задачи конкретного проекта
?assignee=3 — задачи конкретного исполнителя
?due_date_before=2026-12-31 — срок до даты
?due_date_after=2026-06-01 — срок после даты
?is_overdue=true — просроченные задачи
"""
# Фильтры с точным совпадением — берут значение из TextChoices
status = django_filters.ChoiceFilter(choices=Task.Status.choices)
priority = django_filters.ChoiceFilter(choices=Task.Priority.choices)
# FK-фильтры — сравнивают по ID связанного объекта
project = django_filters.NumberFilter(field_name="project__id")
assignee = django_filters.NumberFilter(field_name="assignee__id")
# Диапазон дат
due_date_before = django_filters.DateFilter(
field_name="due_date", lookup_expr="lte"
)
due_date_after = django_filters.DateFilter(
field_name="due_date", lookup_expr="gte"
)
class Meta:
model = Task
fields = ["status", "priority", "project", "assignee"]
5. Обновление TaskViewSet — подключение фильтрации
# apps/tasks/views.py
from rest_framework import viewsets
from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django_filters.rest_framework import DjangoFilterBackend
from .filters import TaskFilter
from .models import Task, Comment
from .serializers import (
TaskListSerializer, TaskDetailSerializer, CommentSerializer,
)
class TaskViewSet(viewsets.ModelViewSet):
"""
CRUD для задач с поддержкой фильтрации, поиска, сортировки и пагинации.
Фильтрация: ?status=todo&priority=high&project=1&assignee=2
Поиск: ?search=авторизация
Сортировка: ?ordering=-created_at (минус = убывание)
Пагинация: ?page=2&page_size=10
"""
permission_classes = [IsAuthenticatedOrReadOnly]
# Бекенды фильтрации для этого ViewSet
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
# FilterSet-класс из filters.py
filterset_class = TaskFilter
# Поля для ?search= — ищет подстроку в title и description
search_fields = ["title", "description"]
# Поля для ?ordering= — разрешённые для сортировки
ordering_fields = ["created_at", "updated_at", "due_date", "priority", "status"]
# Сортировка по умолчанию (новые задачи первыми)
ordering = ["-created_at"]
def get_queryset(self):
return (
Task.objects
.filter(is_deleted=False)
.select_related("project", "assignee")
.prefetch_related("comments")
)
def get_serializer_class(self):
if self.action == "list":
return TaskListSerializer
return TaskDetailSerializer
def perform_create(self, serializer):
serializer.save()
def perform_destroy(self, instance: Task):
"""Soft-delete: помечаем is_deleted=True, не удаляем из БД."""
instance.is_deleted = True
instance.save(update_fields=["is_deleted", "updated_at"])
class CommentViewSet(viewsets.ModelViewSet):
"""CRUD для комментариев."""
serializer_class = CommentSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, OrderingFilter]
filterset_fields = ["task"]
ordering_fields = ["created_at"]
ordering = ["created_at"]
def get_queryset(self):
return Comment.objects.select_related("author", "task")
- SearchFilter — один параметр
?search=, ищет подстроку сразу в нескольких полях через OR. Используй для «умного поиска». - DjangoFilterBackend — отдельный параметр для каждого поля (
?status=todo), точное совпадение или заданный lookup. Используй для «точных фильтров».
6. Кастомный PageSize через query_param (опционально)
Если хочется позволить клиенту задавать размер страницы через ?page_size=10,
можно создать кастомный класс пагинации:
# apps/tasks/pagination.py
from rest_framework.pagination import PageNumberPagination
class TaskPagination(PageNumberPagination):
"""
Пагинация задач с возможностью задать page_size через query_param.
Примеры:
GET /api/tasks/?page=1 → 20 записей (по умолчанию)
GET /api/tasks/?page=1&page_size=5 → 5 записей
GET /api/tasks/?page=1&page_size=100 → не больше max_page_size
"""
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
Подключаем в TaskViewSet:
from .pagination import TaskPagination
class TaskViewSet(viewsets.ModelViewSet):
pagination_class = TaskPagination
# ... остальные атрибуты без изменений ...
🧠 Объяснение логики
Как django-filter обрабатывает запрос
Когда клиент отправляет GET /api/tasks/?status=todo&priority=high,
DRF проходит по списку filter_backends:
DjangoFilterBackendберётfilterset_class = TaskFilterи применяет фильтры из URL-параметров к querysetSearchFilterпроверяет наличие?search=и фильтрует черезLIKE '%...%'по полям изsearch_fieldsOrderingFilterпроверяет?ordering=и вызывает.order_by()на queryset
Все три бекенда работают вместе — фильтры не взаимоисключают друг друга.
ChoiceFilter vs CharFilter
Используем ChoiceFilter, а не просто CharFilter, потому что
в TextChoices допустимы только конкретные значения. ChoiceFilter автоматически
возвращает 400 Bad Request, если передан недопустимый вариант — это явная валидация входных данных.
Пагинация и структура ответа
С включённой пагинацией список задач больше не возвращает массив напрямую. Ответ оборачивается в объект:
{
"count": 47,
"next": "http://127.0.0.1:8000/api/tasks/?page=3",
"previous": "http://127.0.0.1:8000/api/tasks/?page=1",
"results": [ ... ]
}
count — общее число записей до пагинации (после фильтрации).
next/previous — URL следующей/предыдущей страницы.
results — записи текущей страницы.
[]
в объект {"count": ..., "results": []}. Если у вас уже есть клиент,
который ожидает массив — его нужно обновить. Закладывайте пагинацию с самого начала,
не добавляйте постфактум без версионирования API.
select_related и фильтрация
get_queryset() возвращает queryset с select_related.
django-filter применяет фильтры поверх этого queryset через дополнительные
.filter()-вызовы. Django ORM объединяет все условия в один SQL-запрос.
Производительность не страдает.
✅ Проверка
1. Запуск сервера
python manage.py runserver
2. Пагинация — список задач
curl -s "http://127.0.0.1:8000/api/tasks/" | python -m json.tool
{
"count": 5,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"title": "Первая задача",
"status": "todo",
"priority": "medium"
}
]
}
3. Фильтрация по статусу
curl -s "http://127.0.0.1:8000/api/tasks/?status=todo" | python -m json.tool
4. Комбинированная фильтрация
curl -s "http://127.0.0.1:8000/api/tasks/?status=todo&priority=high&ordering=-created_at" | python -m json.tool
5. Полнотекстовый поиск
curl -s "http://127.0.0.1:8000/api/tasks/?search=авторизация" | python -m json.tool
6. Пагинация с кастомным размером страницы
curl -s "http://127.0.0.1:8000/api/tasks/?page=1&page_size=5" | python -m json.tool
Диагностика ошибок
ImproperlyConfigured: django_filters not found— добавьте"django_filters"вINSTALLED_APPSAssertionError: filterset_class requires...— проверьте импортTaskFilterвviews.py- 400 при
?status=INVALID— это нормально,ChoiceFilterотклоняет невалидные значения - Пагинация не работает — убедитесь, что
DEFAULT_PAGINATION_CLASSзадан вREST_FRAMEWORK
➡️ Что дальше
Задачи теперь фильтруемы, поддерживают поиск и пагинацию. Следующий шаг — защита API через JWT-аутентификацию. Без неё любой может создать, изменить или удалить данные. SimpleJWT даёт нам access + refresh токены в стиле OAuth 2.0.
- Готово: фильтрация по status/priority/project/assignee, поиск, сортировка, пагинация
- Шаг 08: SimpleJWT — установка, настройка, регистрация и логин, кастомный payload