Бытует мнение, что у котиков с доминирующим белым окрасом голубой цвет глаз более глубокий, чем у остальных, и это считается достоинством. Вот именно они и представляют интерес для наших пользователей.
В связи с повышенным спросом на белых котиков требуется расширить возможности API: по специальному запросу нужно отдавать информацию о пяти последних добавленных котиках белого цвета. Для таких запросов уже придумали эндпоинт: cats/recent-white-cats/
.
Задача не выглядит типичной: можно получить данные о всех котиках или о каком-то одном, но не о пяти, да ещё и определённого цвета.
Нестандартные действия во вьюсетах
Ещё раз переберём список стандартных действий (англ. actions) во вьюсетах:
- create: создание экземпляра;
- retrieve: получение экземпляра;
- list: получение списка экземпляров;
- update: обновление экземпляра (все поля);
- partial_update: обновление экземпляра (только выбранные поля);
- destroy: удаление экземпляра.
Для поставленной задачи ни один из них не подойдёт.Необходимо нестандартное действие; для этого во вьюсете пишут отдельные методы, которые оборачивают в декоратор @action
(«действие»). Этот декоратор настраивает метод и создаёт эндпоинты для этих действий.
Имя такого метода-«действия» может быть произвольным.Декоратор @action
по умолчанию отслеживает только GET-запрос. Но если передать в декоратор параметр methods
, то можно разрешить и другие методы запросов.
@action(methods=['get', 'delete', ...]
В декораторе можно явным образом указать, должен ли метод работать с одним объектом или с коллекцией объектов. Для этого используется параметр detail
, который может принимать значения True
(разрешена работа с одним объектом) или False
(работаем с коллекцией).
URL эндпоинта по умолчанию генерируется из двух частей: <URL-префикс ресурса>/<название метода>/
.
# К такому методу можно обратиться через эндпоинт cats/cats_for_sale/
@action()
def cats_for_sale()
...
Если URL эндпоинта не должен совпадать с именем метода, URL можно изменить: для этого нужно передать в декоратор аргумент url_path
с необходимым значением.
Решим задачу с получением последних пяти белых котиков. Для этого опишем метод recent_white_cats
для вьюсета CatViewSet
.
from rest_framework.decorators import action
from rest_framework.response import Response
...
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
# Пишем метод, а в декораторе разрешим работу со списком объектов
# и переопределим URL на более презентабельный
@action(detail=False, url_path='recent-white-cats')
def recent_white_cats(self, request):
# Нужны только последние пять котиков белого цвета
cats = Cat.objects.filter(color='White')[:5]
# Передадим queryset cats сериализатору
# и разрешим работу со списком объектов
serializer = self.get_serializer(cats, many=True)
return Response(serializer.data)
Готово, проверяем.
При отправке GET-запроса на эндпоинт cats/recent-white-cats/
должен быть получен примерно такой результат (конечно, если у вас в базе есть достаточно белых котиков):
Различные сериализаторы для одного вьюсета
В Kittygram для сериализации и десериализации каждой модели использовался лишь один сериализатор. Но если некоторые поля сериализатора доступны только для чтения, то по факту для чтения применяется один набор полей, а для записи — другой. Это равносильно использованию разных сериализаторов.
Да, так тоже можно: для модели можно описать несколько сериализаторов и использовать их, например, в зависимости от типа запроса.Допустим, когда
- добавляется новый котик,
- запрашивается детальная информация о конкретном котике,
- обновляется информация о конкретном котике,
будем обрабатывать все доступные поля модели, а вот если запрашивается список котиков, то необходимы только id, имя и цвет.
Опишем для этого ещё один сериализатор, который назовём CatListSerializer
:
class CatListSerializer(serializers.ModelSerializer):
color = serializers.ChoiceField(choices=CHOICES)
class Meta:
model = Cat
fields = ('id', 'name', 'color')
Добавьте во вьюсет стандартный метод get_serializer_class
: в нём можно определить, какой из доступных сериализаторов должен обрабатывать данные в зависимости от действия:
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
...
def get_serializer_class(self):
# Если запрошенное действие (action) — получение списка объектов ('list')
if self.action == 'list':
# ...то применяем CatListSerializer
return CatListSerializer
# А если запрошенное действие — не 'list', применяем CatSerializer
return CatSerializer
Проверим результаты, отправив GET-запрос на получение списка всех котиков.
По ответу видно, что отработал новый сериализатор: всё по плану. А при запросе к конкретному котику всё работает как и раньше:
Нестандартный набор стандартных действий
Чаще всего мы наследовали вьюсеты от ModelViewSet
. По умолчанию это даёт возможность обрабатывать шесть типичных действий:
- создание нового объекта;
- получение информации об одном объекте;
- удаление объекта;
- полное замещение существующего объекта;
- изменение одного или нескольких полей объекта;
- получение списка объектов.
Если требуется только получать данные из БД, можно унаследоваться от ReadOnlyModelViewSet
. В этом случае доступный набор действий будет таким:
- получение информации об одном объекте;
- получение списка объектов.
А если требуется иной набор действий — например, нужно только создавать новый объект и получать информацию об одном объекте? Эта задача может быть решена с использованием миксинов (от англ. mix in, «смешивать»).
Миксины и ваш собственный базовый класс для вьюсетов
Никто не знает, что придёт в голову разработчикам и какие вьюсеты им понадобятся, поэтому готовых вьюсетов на все случаи жизни нет и быть не может. Но есть классы-«детали», из которых можно быстро собрать практически любой необходимый базовый вьюсет: миксины.
Чтобы самостоятельно создать базовый вьюсет с особым набором действий — нужно унаследовать его от одного или нескольких миксинов с нужными действиями и, дополнительно, от базового класса GenericViewSet
:
from rest_framework import mixins
...
# Собираем вьюсет, который будет уметь изменять или удалять отдельный объект.
# А ничего больше он уметь не будет.
class UpdateDeleteViewSet(mixins.UpdateModelMixin, mixins.DestroyModelMixin,
viewsets.GenericViewSet):
pass
В DRF есть пять предустановленных классов миксинов, они соответствуют пяти операциям с данными:
- CreateModelMixin — создать объект (для обработки запросов POST);
- ListModelMixin — вернуть список объектов (для обработки запросов GET);
- RetrieveModelMixin — вернуть объект (для обработки запросов GET);
- UpdateModelMixin — изменить объект (для обработки запросов PUT и PATCH);
- DestroyModelMixin — удалить объект (для обработки запросов DELETE).
Опишем собственный базовый класс вьюсета: он будет создавать экземпляр объекта и получать экземпляр объекта; назовём его CreateRetrieveViewSet
.
# cats/views.py
...
from rest_framework import mixins
...
class CreateRetrieveViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
# В теле класса никакой код не нужен! Пустячок, а приятно.
pass
Базовый вьюсет готов, он получился не хуже, чем ModelViewSet
. Теперь можно унаследоваться от этого базового класса.Опишем вьюсет LightCatViewSet
, унаследованный от CreateRetrieveViewSet
.
class LightCatViewSet(CreateRetrieveViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
Зарегистрируем этот вьюсет в роутере:
from cats.views import CatViewSet, LightCatViewSet, OwnerViewSet
...
router.register(r'mycats', LightCatViewSet)
Нужно разобраться, что за странный синтаксис в первом аргументе метода register()
, ведь там должен быть просто префикс для URL.
Неожиданное откровение
При регистрации эндпоинтов для вьюсета в качестве URL-префикса используется не обычная строка, а регулярное выражение. За счёт этого URL-префикс может обрабатывать множество вариантов URL.
...
# При регистрации эндпоинтов с таким URL-префиксом
router.register(r'profile/(?P<username>[\w.@+-]+)/', AnyViewSet)
# ...вьюсет AnyViewSet будет получать на обработку все запросы с адресов
# /profile/toh@/
# /profile/nik.nik/
# /profile/leo/
# ...и подобных, ограниченных маской регулярного выражения.
В уроке о роутерах в первый параметр метода register()
мы передавали просто строку, но «просто строка» — это частный случай регулярного выражения, и Django эту «просто строку» тоже интерпретировал как regExp.
Префикс r
перед строкой определяет raw-строку (или r-строку, так короче). Такую строку система будет читать как простую последовательность символов, игнорируя escape-последовательности (англ. escape sequence) — комбинации обратного слеша и символа.
Например, escape-последовательность \n
интерпретируется как перенос строки, но в raw-строке это будут просто два текстовых символа без всякого скрытого смысла; в результате вся r-строка будет считана как регулярное выражение.
Вернёмся к нашим миксинам
Базовый вьюсет собран, наследник описан и применён в коде.
Проверьте работу нового вьюсета: сделайте запросы на новый эндпоинт mycats/
.
GET-запрос к конкретному котику отрабатывает на «отлично»:
А вот получить список котиков через этот эндпоинт нельзя:
Работает! Теперь можно монтировать базовые вьюсеты с любым набором действий.
Мой вьюсет — мои правила: наследование от класса ViewSet
В тех случаях, когда нужно получить больше контроля и возможностей что-то «подкрутить» во вьюсетах, можно наследоваться от базового класса ViewSet
. Именно от него наследуется, например, класс ModelViewSet
.Класс ViewSet
, в свою очередь, наследуется от APIView
.
При работе с низкоуровневыми вьюсетами все нужные методы вам придётся описать самостоятельно.
В классе ViewSet
есть шесть предопределённых методов:
- list(self, request) — для получения списка объектов из queryset;
- create(self, request) — для создания объекта в модели;
- retrieve(self, request, pk=None) — для получения определённого объекта из queryset;
- update(self, request, pk=None) — для перезаписи (полного обновления) определённого объекта из queryset;
- partial_update(self, request, pk=None) — для частичного обновления объекта из queryset;
- destroy(self, request, pk=None) — для удаления одного из объектов queryset.
Чтобы применить любой из этих методов, нужно полностью описать его; в классе ViewSet
эти методы объявлены, но не описаны.
Например, в приложении нужно создать вьюсет, который будет получать сериализованный объект одного котика (методом retrieve()
) и полный список всех котиков (методом list()
). Это можно сделать так:
from rest_framework import viewsets
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from .models import Cat
from .serializers import CatSerializer
class CatViewSet(viewsets.ViewSet):
def list(self, request):
queryset = Cat.objects.all()
serializer = CatSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = Cat.objects.all()
cat = get_object_or_404(queryset, pk=pk)
serializer = CatSerializer(cat)
return Response(serializer.data)
Похоже на то, как мы работали в APIView, с той разницей, что здесь присутствуют поля queryset и serializer.
Документацию по предустановленным методам класса ViewSet
можно посмотреть на официальном сайте проекта.