Django: Как создать фильтр на основе представлений-классов? Пошаговое руководство

В современных веб-приложениях возможность быстро находить нужные данные среди большого объема информации является критически важной. Фильтрация списков объектов — одна из наиболее часто реализуемых функций. В Django для создания представлений традиционно использовались функции (FBV), но с появлением представлений-классов (CBV) многие задачи, включая фильтрацию, могут быть решены более структурированно и переиспользуемо.

Преимущества использования классов представлений для фильтрации

Использование классов представлений, в частности ListView, для реализации фильтрации предоставляет ряд преимуществ:

Структура и организация: Логика разбивается на методы класса (get_queryset, get_context_data), что улучшает читаемость и поддерживаемость кода.

Переиспользуемость: Общие паттерны (например, обработка формы в get_queryset) могут быть вынесены в миксины.

Наследование: CBV легко расширять, добавляя или изменяя функциональность через наследование.

Консистентность: Единый подход к реализации различных представлений в проекте.

Обзор стандартных подходов к фильтрации в Django

Исторически фильтрация в Django могла реализовываться несколькими способами:

Вручную в FBV: Получение GET-параметров из request.GET и применение методов filter(), exclude() к менеджеру модели. Этот подход быстро становится громоздким при увеличении числа полей для фильтрации.

С использованием сторонних библиотек: Например, django-filter, которая предоставляет готовые классы для создания мощных и гибких фильтров с минимальным количеством кода. Это отличный вариант для сложных случаев, но иногда требуется более тонкий контроль над логикой.

На основе CBV: Интеграция формы фильтрации с ListView, переопределяя его методы. Этот подход предлагает баланс между гибкостью ручной реализации и структурой CBV, позволяя полностью контролировать логику фильтрации.

В данной статье мы сосредоточимся именно на реализации фильтрации с использованием стандартных классов представлений Django, показав, как интегрировать форму и динамически изменять queryset.

Постановка задачи: разработка фильтра для списка объектов

В качестве практического примера мы разработаем фильтр для списка товаров (Product). Фильтр позволит искать товары по названию (частичное совпадение), категории и диапазону цен.

Подготовка проекта Django и модели данных

Для начала работы нам потребуется минимально настроенный проект Django и приложение.

Создание нового проекта Django и приложения

Предполагается, что у вас уже установлен Django. Создадим новый проект и приложение:

django-admin startproject myproject .
python manage.py startapp products

Не забудьте добавить products в INSTALLED_APPS в myproject/settings.py.

Определение модели данных (пример: модель ‘Product’)

Определим простую модель Product в файле products/models.py:

from django.db import models

class Category(models.Model):
    name: str = models.CharField(max_length=100, unique=True)

    def __str__(self) -> str:
        return self.name

class Product(models.Model):
    name: str = models.CharField(max_length=200, db_index=True)
    description: str = models.TextField(blank=True)
    price: models.DecimalField = models.DecimalField(max_digits=10, decimal_places=2)
    category: models.ForeignKey = models.ForeignKey(
        Category, 
        related_name='products', 
        on_delete=models.CASCADE
    )
    available: bool = models.BooleanField(default=True)
    created: models.DateTimeField = models.DateTimeField(auto_now_add=True)
    updated: models.DateTimeField = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ('name',)

    def __str__(self) -> str:
        return self.name

Здесь мы определили модель Product с полями для названия, цены, категории и наличия, а также вспомогательную модель Category.

Миграция базы данных

Применим изменения к базе данных:

python manage.py makemigrations
python manage.py migrate

Заполнение базы данных тестовыми данными

Для удобства тестирования создадим несколько категорий и товаров. Это можно сделать через Django Shell:

python manage.py shell
# products/management/commands/populate_products.py (или просто в shell)
from products.models import Category, Product

# Очистка данных (опционально)
# Category.objects.all().delete()
# Product.objects.all().delete()

# Создание категорий
category_electronics, _ = Category.objects.get_or_create(name='Electronics')
category_books, _ = Category.objects.get_or_create(name='Books')
category_clothing, _ = Category.objects.get_or_create(name='Clothing')

# Создание товаров
Product.objects.get_or_create(
    name='Laptop', price='1200.00', category=category_electronics
)
Product.objects.get_or_create(
    name='Smartphone', price='800.00', category=category_electronics
)
Product.objects.get_or_create(
    name='Book "Django for Professionals"', price='45.00', category=category_books
)
Product.objects.get_or_create(
    name='T-Shirt', price='25.00', category=category_clothing
)
Product.objects.get_or_create(
    name='Headphones', price='150.00', category=category_electronics, available=False
)

print("Test data populated.")
# exit()

Это позволит иметь набор данных для проверки работы фильтра.

Реализация фильтрации с использованием классов представлений

Теперь перейдем к основной части — созданию представления списка и интеграции фильтрации.

Создание базового представления списка объектов (ListView)

Определим простое ListView для отображения списка товаров в products/views.py:

from django.views.generic import ListView
from .models import Product

class ProductListView(ListView):
    model = Product
    template_name = 'products/product_list.html'
    context_object_name = 'products'
    # paginate_by = 10 # Можно добавить пагинацию при необходимости

Создадим соответствующий URL-шаблон в products/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.ProductListView.as_view(), name='product_list'),
]

Подключим URL-шаблоны приложения в главном myproject/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('products/', include('products.urls')),
]

Создадим базовый шаблон products/product_list.html для отображения списка (упрощенно):


Product List

    {% for product in products %}
  • {{ product.name }} - ${{ product.price }} (Category: {{ product.category.name }})
  • {% empty %}
  • No products found.
  • {% endfor %}

На этом этапе по адресу /products/ будет доступен список всех товаров.

Добавление формы фильтрации (FilterForm)

Создадим форму, которая будет содержать поля для ввода критериев фильтрации, в файле products/forms.py:

from django import forms
from .models import Category

class ProductFilterForm(forms.Form):
    # Поле для поиска по названию (частичное совпадение)
    name: forms.CharField = forms.CharField(
        max_length=100, 
        required=False, 
        label='Name contains'
    )
    
    # Поле для выбора категории
    category: forms.ModelChoiceField = forms.ModelChoiceField(
        queryset=Category.objects.all(), 
        required=False, 
        empty_label='Any category'
    )
    
    # Поле для минимальной цены
    min_price: forms.DecimalField = forms.DecimalField(
        max_digits=10, 
        decimal_places=2, 
        required=False, 
        label='Min Price'
    )
    
    # Поле для максимальной цены
    max_price: forms.DecimalField = forms.DecimalField(
        max_digits=10, 
        decimal_places=2, 
        required=False, 
        label='Max Price'
    )
    
    # Поле для фильтрации по наличию (опционально, можно использовать CheckboxInput)
    # available: forms.BooleanField(
    #     required=False,
    #     label='Is Available'
    # )
    
    # Пример поля для чекбокса наличия:
    available_only: forms.BooleanField(
        required=False,
        label='Show only available',
        widget=forms.CheckboxInput
    )

    # clean_... методы могут быть добавлены для валидации диапазонов и т.д.
    def clean(self):
        cleaned_data = super().clean()
        min_price = cleaned_data.get('min_price')
        max_price = cleaned_data.get('max_price')

        # Пример базовой валидации диапазона цен
        if min_price is not None and max_price is not None and min_price > max_price:
            # Добавлять ошибку к конкретным полям лучше, но для примера: к форме в целом
            raise forms.ValidationError("Min price cannot be greater than max price.")
            
        return cleaned_data

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

Интеграция формы фильтрации в представление (переопределение метода get_queryset)

Теперь модифицируем ProductListView для использования этой формы. Основная логика фильтрации будет размещена в методе get_queryset. Также добавим форму в контекст шаблона с помощью get_context_data.

from django.views.generic import ListView
from django.db.models import Q
from django.http import HttpRequest
from django.db.models import QuerySet

from .models import Product
from .forms import ProductFilterForm

class ProductListView(ListView):
    model = Product
    template_name = 'products/product_list.html'
    context_object_name = 'products'
    # paginate_by = 10

    def get_queryset(self) -> QuerySet[Product]:
        # Получаем базовый queryset
        queryset = super().get_queryset()

        # Создаем экземпляр формы, заполняя его данными из GET-запроса
        form = ProductFilterForm(self.request.GET)

        # Проверяем валидность формы. Это также выполнит очистку данных.
        if form.is_valid():
            # Получаем очищенные данные из формы
            cleaned_data = form.cleaned_data

            # Применяем фильтры на основе очищенных данных
            name = cleaned_data.get('name')
            category = cleaned_data.get('category')
            min_price = cleaned_data.get('min_price')
            max_price = cleaned_data.get('max_price')
            available_only = cleaned_data.get('available_only')

            if name:
                # Поиск по части названия (case-insensitive)
                queryset = queryset.filter(name__icontains=name)

            if category:
                # Фильтр по категории
                queryset = queryset.filter(category=category)

            if min_price is not None:
                # Фильтр по минимальной цене (больше или равно)
                queryset = queryset.filter(price__gte=min_price)

            if max_price is not None:
                # Фильтр по максимальной цене (меньше или равно)
                queryset = queryset.filter(price__lte=max_price)
                
            if available_only:
                 # Фильтр по наличию
                 queryset = queryset.filter(available=True)

            # Важно: Если форма невалидна, фильтры не применяются, 
            # но форма с ошибками будет доступна в контексте для отображения.

        # Возвращаем отфильтрованный или исходный queryset
        return queryset

    def get_context_data(self, **kwargs) -> dict:
        # Получаем стандартный контекст
        context = super().get_context_data(**kwargs)
        
        # Создаем экземпляр формы, заполняя его данными из GET-запроса
        # Это нужно, чтобы при отображении страницы форма сохраняла введенные
        # пользователем значения фильтров после отправки.
        context['filter_form'] = ProductFilterForm(self.request.GET)
        
        # Возвращаем обновленный контекст
        return context
Реклама

В методе get_queryset мы получаем базовый queryset, затем создаем экземпляр ProductFilterForm, передавая ему данные из request.GET. Если форма валидна, мы извлекаем очищенные данные (cleaned_data) и используем их для последовательного применения фильтров к queryset. Обратите внимание, что каждый вызов .filter() возвращает новый queryset, позволяя цепочкой применять условия.

В get_context_data мы добавляем экземпляр той же формы, также заполненный данными из request.GET, в контекст шаблона. Это гарантирует, что когда шаблон будет отрендерен, поля формы будут содержать те значения, которые ввел пользователь до отправки запроса.

Обработка данных формы фильтрации и применение фильтров к queryset

Как показано в коде выше, обработка данных происходит внутри get_queryset:

form = ProductFilterForm(self.request.GET): Создается форма с данными GET-запроса.

if form.is_valid():: Выполняется валидация и очистка данных. Если данные некорректны (например, неверный формат числа для цены), is_valid() вернет False, и блок фильтрации выполнен не будет. Ошибки формы будут доступны в шаблоне.

cleaned_data = form.cleaned_data: Доступ к проверенным и преобразованным (например, строка в число/Decimal) данным.

queryset = queryset.filter(...): Последовательное применение методов filter() на основе cleaned_data. Например, name__icontains=name использует icontains lookup для регистронезависимого поиска по подстроке, price__gte и price__lte используют gte (greater than or equal) и lte (less than or equal) lookups для диапазона цен.

Теперь обновим шаблон products/product_list.html, чтобы добавить форму фильтрации:


Product List

{{ filter_form.as_p }} Reset Filter
    {% for product in products %}
  • {{ product.name }} - ${{ product.price }} (Category: {{ product.category.name }}{% if not product.available %} - Not Available{% endif %})
  • {% empty %}
  • No products found.
  • {% endfor %}

Форма отправляется методом GET. При отправке, данные формы попадают в request.GET, откуда они считываются в представлении для фильтрации. Ссылка "Reset Filter" просто ведет на ту же страницу без GET-параметров, сбрасывая тем самым фильтрацию.

Расширенные возможности и оптимизация

Созданный фильтр уже функционален, но его можно улучшить.

Использование Q-объектов для сложных фильтров

Если вам нужно реализовать логику с OR условиями или более сложными комбинациями AND/OR, используйте Q объекты из django.db.models.

Пример: поиск товаров, у которых название или описание содержит подстроку:

from django.db.models import Q

# ... в методе get_queryset

if name:
    # Использование Q-объекта для OR-условия
    queryset = queryset.filter(Q(name__icontains=name) | Q(description__icontains=name))

# ... остальные фильтры

Q объекты можно комбинировать с помощью операторов & (AND), | (OR) и ~ (NOT), а затем передавать их в метод filter() или exclude().

Кэширование результатов фильтрации

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

Более реалистичные сценарии кэширования в контексте фильтрации CBV:

Кэширование формы или данных для ее полей (например, список категорий, если их очень много и они редко меняются). Это можно сделать в get_context_data или get_form (если используете кастомный метод формы).

Кэширование базового QuerySet перед применением фильтров, если его получение ресурсоемко (редко).

Кэширование всего HTML-вывода представления, если запросы с одинаковыми параметрами фильтрации повторяются часто. Это можно реализовать на уровне URLconf или с помощью декоратора cache_page.

# myproject/urls.py
from django.urls import path, include
from django.views.decorators.cache import cache_page
from products.views import ProductListView

urlpatterns = [
    path('admin/', admin.site.urls),
    # Кэширование страницы на 15 минут (900 секунд)
    path('products/', cache_page(900)(ProductListView.as_view()), name='product_list'),
]

Важно понимать, что при кэшировании страницы с параметрами GET, Django кэширует страницу для каждой уникальной комбинации GET-параметров. Это может привести к большому потреблению памяти кэша при множестве вариаций фильтров.

Валидация и очистка данных формы фильтрации

Использование Django Forms для обработки данных фильтрации автоматически обеспечивает валидацию и очистку данных (преобразование строк из GET-запроса в соответствующие типы Python: Decimal, Integer, Boolean, Model instances и т.д.).

Метод form.is_valid() выполняет эту работу. Добавление кастомной логики валидации в clean() или clean_fieldname() методы формы (products/forms.py) позволяет проверять более сложные условия, например, корректность диапазона цен (min_price <= max_price), как было показано в примере формы.

Это не только защищает от некорректных данных и потенциальных SQL-инъекций (хотя ORM уже предоставляет защиту), но и упрощает работу с данными фильтров в get_queryset, так как вы оперируете уже типизированными значениями.

Заключение

Мы рассмотрели, как реализовать функциональность фильтрации для списка объектов в Django, используя классы представлений, в частности ListView, в комбинации с Django Forms.

Обзор изученных концепций и техник

Использование ListView как основы для отображения списка.

Создание отдельной forms.Form для описания полей фильтрации.

Передача данных request.GET в форму для ее заполнения.

Переопределение метода get_queryset в ListView для применения фильтров на основе данных валидированной формы.

Использование form.cleaned_data для доступа к очищенным данным.

Добавление формы фильтрации в контекст шаблона через get_context_data.

Краткий обзор Q объектов для сложных запросов и возможностей кэширования.

Преимущества и недостатки подхода на основе классов представлений

Преимущества:

Структурированность: Четкое разделение логики.

Использование стандартных механизмов Django: Формы для валидации и обработки данных, CBV для структуры представления.

Контроль: Полный контроль над процессом фильтрации.

Недостатки:

Многословность: Для большого количества полей фильтрации или сложной логики get_queryset может стать довольно длинным.

Повторение кода: Может потребоваться писать похожую логику фильтрации в разных представлениях или для разных наборов полей.

Дальнейшие направления для изучения и улучшения фильтрации

Использование django-filter: Для сложных фильтров с множеством полей и разнообразными lookups, эта библиотека значительно сокращает объем кода.

Создание миксинов: Вынесение общей логики обработки формы фильтрации в переиспользуемый миксин для CBV.

Реализация AJAX-фильтрации: Динамическое обновление списка без перезагрузки страницы с использованием JavaScript и отдельного endpoint’а или того же представления, возвращающего JSON или фрагмент HTML.

Полнотекстовый поиск: Для более продвинутого поиска по текстовым полям рассмотрите интеграцию с поисковыми движками (например, Elasticsearch, PostgreSQL FTS) или использование SearchVector, SearchQuery в Django ORM.

Данный подход с использованием ListView и Django Forms является мощным и гибким инструментом для создания управляемой фильтрации, позволяя глубоко понимать и контролировать процесс обработки запросов и формирования списков данных.


Добавить комментарий