Вьюсеты в Django. Расширенные возможности

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

В связи с повышенным спросом на белых котиков требуется расширить возможности 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 можно посмотреть на официальном сайте проекта.





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

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: