📖 Теория — Урок 33

← К оглавлению урока

⚡ Ключевые концепции урока

  • lookup_field — какое поле модели использовать для поиска (по умолчанию pk)
  • lookup_url_kwarg — какой параметр URL соответствует lookup_field
  • get_object() — переопределить для кастомной фильтрации и обработки ошибок
  • get_serializer_context() — передать параметры запроса в сериализатор через контекст
  • filter_backends — список бэкендов: DjangoFilterBackend, SearchFilter, OrderingFilter
  • filterset_fields — поля для точной фильтрации (?author=...)
  • search_fields — поля для текстового поиска (?search=...)
  • ordering_fields — поля для сортировки (?ordering=price)

Часть 1: Атрибуты lookup_field и lookup_url_kwarg

Атрибуты lookup_field и lookup_url_kwarg позволяют определять, какое поле модели и параметр URL будут использоваться для поиска объекта при обработке запросов. Эти атрибуты полезны, когда нужно идентифицировать объекты по полям, отличным от стандартного первичного ключа (pk).

По умолчанию: если lookup_field не задан, поиск выполняется по полю pk. Если lookup_url_kwarg не задан, используется значение lookup_field.

Назначение атрибутов

АтрибутНазначение
lookup_field Указывает поле модели, по которому будет выполняться поиск объекта
lookup_url_kwarg Указывает параметр URL, из которого берётся значение для поиска. Если не задан — совпадает с lookup_field

Пример: поиск жанра по названию

Стандартный подход — поиск по pk: GET /genres/3/. С lookup_field = 'name' можно обращаться к жанру по имени: GET /genres/comedy/.

# views.py
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from .models import Genre
from .serializers import GenreSerializer

class GenreDetailUpdateDeleteView(RetrieveUpdateDestroyAPIView):
    queryset = Genre.objects.all()
    serializer_class = GenreSerializer
    lookup_field = 'name'          # поле модели для поиска
    lookup_url_kwarg = 'genre_name'  # параметр URL (из path-конвертора)
# urls.py
from django.urls import path
from .views import GenreDetailUpdateDeleteView

urlpatterns = [
    path('genres/<str:genre_name>/', GenreDetailUpdateDeleteView.as_view(),
         name='genre-detail-update-delete'),
]

Запрос и ответ:

GET http://127.0.0.1:8000/genres/comedy/

{
    "id": 3,
    "name": "Comedy"
}

Применение

  • Гибкость идентификации: идентифицировать объекты по slug, email, username или любому уникальному полю
  • Читабельные URL: /users/john_doe/ вместо /users/42/

Часть 2: Метод get_object()

Метод get_object() — важный компонент Generic Views, который используется для получения одного экземпляра модели на основе определённого критерия поиска. Переопределяя этот метод, можно добавить кастомную логику фильтрации, обработки ошибок и дополнительных проверок.

Основное назначение

  • Поиск объекта: get_object() извлекает объект модели на основе значения из URL
  • Фильтрация объектов: можно исключать запрещённые или мягко удалённые элементы
  • Обработка ошибок: кастомная логика при отсутствии объекта

Пример: фильтрация запрещённых книг

Добавим в модель Book поле is_banned, чтобы скрывать запрещённые книги через API:

# models.py
from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    published_date = models.DateField()
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE,
                                  null=True, blank=True)
    created_at = models.DateTimeField(null=True, blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2,
                                null=True, blank=True)
    discounted_price = models.DecimalField(max_digits=10, decimal_places=2,
                                           null=True, blank=True)
    is_bestseller = models.BooleanField(default=False)
    genres = models.ManyToManyField(Genre, related_name='books')
    is_banned = models.BooleanField(default=False)  # запрещённая книга
# views.py
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from rest_framework.exceptions import NotFound
from .models import Book
from .serializers import BookSerializer

class BookDetailUpdateDeleteView(RetrieveUpdateDestroyAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def get_object(self):
        # Получение параметра pk из URL
        pk = self.kwargs.get('pk')

        # Попытка найти объект по pk, исключая запрещённые
        try:
            book = self.queryset.get(pk=pk, is_banned=False)
        except Book.DoesNotExist:
            # Кастомное сообщение об ошибке
            raise NotFound(detail=f"Book with id '{pk}' not found or is banned.")

        return book
Важно: стандартный get_object() автоматически вызывает self.check_object_permissions(). При переопределении не забудьте вызвать его явно: self.check_object_permissions(self.request, obj), если используете object-level permissions.

Резюме

Использование get_object() с полем is_banned позволяет эффективно фильтровать недоступные объекты. Это улучшает безопасность и контроль над доступностью данных.

Часть 3: Метод get_serializer_context()

Метод get_serializer_context() используется для передачи дополнительного контекста сериализатору. Контекст позволяет сериализатору иметь доступ к информации, которая недоступна из полей модели напрямую — например, параметрам запроса, текущему пользователю или флагам конфигурации.

Основное назначение

  • Передача дополнительной информации: текущий пользователь, параметры запроса, флаги
  • Расширение возможностей сериализатора: динамические поля, условная валидация

Что уже есть в стандартном контексте

Базовый super().get_serializer_context() возвращает словарь с тремя ключами: request, format, view. Расширяя его, мы добавляем свои ключи.

# views.py
from rest_framework.generics import ListAPIView
from .models import Book
from .serializers import BookSerializer

class BookListCreateView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

    def get_serializer_context(self):
        context = super().get_serializer_context()  # не терять request/format/view
        # Передаём флаг из параметра запроса
        context['include_related'] = (
            self.request.query_params.get('include_related', 'false').lower() == 'true'
        )
        return context
# serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

    def to_representation(self, instance):
        representation = super().to_representation(instance)
        # Используем значение из контекста
        if self.context.get('include_related'):
            representation['genres'] = [genre.name for genre in instance.genres.all()]
        else:
            representation.pop('genres', None)
        return representation

Пример запроса и ответа

GET http://127.0.0.1:8000/books/?include_related=true

{
    "id": 1,
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "published_date": "1925-04-10",
    "price": "15.99",
    "discounted_price": "12.99",
    "is_bestseller": false,
    "genres": ["Classic", "Fiction"],
    "publisher": 1,
    "created_at": "2024-06-12T12:34:56.789Z",
    "is_banned": false
}

При запросе без параметра (?include_related=false или без него) поле genres будет убрано из ответа.

Резюме

Метод get_serializer_context() позволяет адаптировать представление данных в зависимости от условий запроса или других факторов. Это делает API более динамичным и предоставляет клиентам возможность получать детализированные или упрощённые ответы.

Часть 4: Атрибут filter_backends

В DRF filter_backends — механизм, позволяющий автоматически фильтровать данные на основе параметров запроса. Это избавляет от необходимости вручную писать логику фильтрации в каждом представлении.

Основное назначение

  • Фильтрация данных: автоматическая фильтрация на основе параметров запроса
  • Удобство: встроенные инструменты без лишнего кода в каждом представлении
  • Расширяемость: поддержка различных бэкендов фильтрации

Три вида фильтрации

БэкендАтрибутПараметр запросаПример
DjangoFilterBackend filterset_fields ?field=value ?author=Fitzgerald
filters.SearchFilter search_fields ?search=text ?search=Gatsby
filters.OrderingFilter ordering_fields ?ordering=field или ?ordering=-field ?ordering=-price

Установка и настройка

Для фильтрации по полям нужен пакет django-filter:

pip install django-filter
# settings.py
INSTALLED_APPS = [
    # ... другие приложения
    'django_filters',
]

Представление с filter_backends

# views.py
from rest_framework.generics import ListAPIView
from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Book
from .serializers import BookSerializer

class BookListCreateView(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    filter_backends = [DjangoFilterBackend, filters.SearchFilter,
                       filters.OrderingFilter]
    filterset_fields = ['author', 'publisher', 'is_bestseller']  # точная фильтрация
    search_fields = ['title', 'author']                          # текстовый поиск
    ordering_fields = ['published_date', 'price']                # сортировка

Примеры запросов

# Фильтрация по автору (точное совпадение)
GET http://127.0.0.1:8000/books/?author=F. Scott Fitzgerald

# Поиск по заголовку или автору (содержит подстроку)
GET http://127.0.0.1:8000/books/?search=Gatsby

# Сортировка по цене (по возрастанию)
GET http://127.0.0.1:8000/books/?ordering=price

# Сортировка по цене (по убыванию, минус перед полем)
GET http://127.0.0.1:8000/books/?ordering=-price

# Комбинирование: фильтрация + сортировка
GET http://127.0.0.1:8000/books/?is_bestseller=true&ordering=-published_date

Резюме

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