Viewsets

ViewSets

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

Ruby on Rails Documentation

DRF позволяет объединить логику для набора связанных представлений в одном классе, называемом ViewSet. В других фреймворках вы также можете встретить концептуально схожие реализации с названиями типа "Ресурсы" или "Контроллеры".

Класс ViewSet - это просто тип представления на основе класса, который не предоставляет никаких обработчиков методов, таких как .get() или .post(), а вместо этого предоставляет такие действия, как .list() и .create().

Обработчики методов для ViewSet привязываются к соответствующим действиям только в момент финализации представления с помощью метода .as_view().

Обычно вместо того, чтобы явно регистрировать представления в наборе представлений в urlconf, вы регистрируете набор представлений в классе маршрутизатора, который автоматически определяет urlconf для вас.

Пример

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

from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404
from myapps.serializers import UserSerializer
from rest_framework import viewsets
from rest_framework.response import Response

class UserViewSet(viewsets.ViewSet):
    """
    A simple ViewSet for listing or retrieving users.
    """
    def list(self, request):
        queryset = User.objects.all()
        serializer = UserSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        queryset = User.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = UserSerializer(user)
        return Response(serializer.data)

Если нужно, мы можем разделить этот набор представлений на два отдельных представления, как показано ниже:

user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})

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

from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls

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

class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset for viewing and editing user instances.
    """
    serializer_class = UserSerializer
    queryset = User.objects.all()

Есть два основных преимущества использования класса ViewSet вместо класса View.

  • Повторяющаяся логика может быть объединена в один класс. В приведенном выше примере нам нужно указать queryset только один раз, и он будет использоваться в нескольких представлениях.

  • Используя маршрутизаторы, нам больше не нужно самим создавать URL conf.

Оба варианта имеют свои преимущества. Использование обычных представлений и URL-конфигураций является более явным и дает вам больше контроля. Наборы представлений полезны, если вы хотите быстро приступить к работе, или если у вас большой API и вы хотите обеспечить последовательную конфигурацию URL во всем.

Действия ViewSet

Маршрутизаторы по умолчанию, входящие в состав DRF, обеспечивают маршруты для стандартного набора действий в стиле create/retrieve/update/destroy, как показано ниже:

class UserViewSet(viewsets.ViewSet):
    """
    Example empty viewset demonstrating the standard
    actions that will be handled by a router class.

    If you're using format suffixes, make sure to also include
    the `format=None` keyword argument for each action.
    """

    def list(self, request):
        pass

    def create(self, request):
        pass

    def retrieve(self, request, pk=None):
        pass

    def update(self, request, pk=None):
        pass

    def partial_update(self, request, pk=None):
        pass

    def destroy(self, request, pk=None):
        pass

Интроспекция действий ViewSet

Во время диспетчеризации для ViewSet доступны следующие атрибуты.

  • basename - основа, используемая для имен создаваемых URL.

  • action - имя текущего действия (например, list, create).

  • detail - булево значение, указывающее, настроено ли текущее действие на просмотр списка или деталей.

  • suffix - суффикс отображения для типа набора представлений - зеркально отражает атрибут detail.

  • name - отображаемое имя набора представлений. Этот аргумент является взаимоисключающим для suffix.

  • description - отображаемое описание для отдельного вида набора представлений.

Вы можете использовать эти атрибуты для настройки поведения в зависимости от текущего действия. Например, вы можете ограничить права на все действия, кроме действия list, следующим образом:

def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    if self.action == 'list':
        permission_classes = [IsAuthenticated]
    else:
        permission_classes = [IsAdminUser]
    return [permission() for permission in permission_classes]

Добавление дополнительных действий в маршрутизацию

Если у вас есть специальные методы, которые должны быть маршрутизируемыми, вы можете пометить их как таковые с помощью декоратора @action. Как и обычные действия, дополнительные действия могут быть предназначены как для одного объекта, так и для целой коллекции. Чтобы указать это, установите аргумент detail в True или False. Маршрутизатор настроит свои шаблоны URL соответствующим образом. Например, DefaultRouter настроит подробные действия так, чтобы они содержали pk в своих шаблонах URL.

Более полный пример дополнительных действий:

from django.contrib.auth.models import User
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer

class UserViewSet(viewsets.ModelViewSet):
    """
    A viewset that provides the standard actions
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=True, methods=['post'])
    def set_password(self, request, pk=None):
        user = self.get_object()
        serializer = PasswordSerializer(data=request.data)
        if serializer.is_valid():
            user.set_password(serializer.validated_data['password'])
            user.save()
            return Response({'status': 'password set'})
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    @action(detail=False)
    def recent_users(self, request):
        recent_users = User.objects.all().order_by('-last_login')

        page = self.paginate_queryset(recent_users)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(recent_users, many=True)
        return Response(serializer.data)

Декоратор action по умолчанию направляет запросы GET, но может принимать и другие HTTP-методы, задав аргумент methods. Например:

    @action(detail=True, methods=['post', 'delete'])
    def unset_password(self, request, pk=None):
       ...

Аргумент methods также поддерживает методы HTTP, определенные как HTTPMethod. Пример ниже идентичен приведенному выше:

from http import HTTPMethod

    @action(detail=True, methods=[HTTPMethod.POST, HTTPMethod.DELETE])
    def unset_password(self, request, pk=None):
        ...

Декоратор позволяет переопределить любую конфигурацию на уровне набора представлений, такую как permission_classes, serializer_class, filter_backends...:

    @action(detail=True, methods=['post'], permission_classes=[IsAdminOrIsSelf])
    def set_password(self, request, pk=None):
       ...

Два новых действия будут доступны по адресам ^users/{pk}/set_password/$ и ^users/{pk}/unset_password/$. Используйте параметры url_path и url_name для изменения сегмента URL и обратного имени URL действия.

Чтобы просмотреть все дополнительные действия, вызовите метод .get_extra_actions().

Маршрутизация дополнительных методов HTTP для дополнительных действий

Дополнительные действия могут сопоставлять дополнительные HTTP-методы с отдельными методами ViewSet. Например, описанные выше методы установки и снятия пароля могут быть объединены в один маршрут. Обратите внимание, что дополнительные сопоставления не принимают аргументов.

    @action(detail=True, methods=["put"], name="Change Password")
    def password(self, request, pk=None):
        """Update the user's password."""
        ...


    @password.mapping.delete
    def delete_password(self, request, pk=None):
        """Delete the user's password."""
        ...

Получение URL-адреса действия

Если вам нужно получить URL-адрес действия, используйте метод .reverse_action(). Это удобная обертка для reverse(), автоматически передающая объект request представления и дополняющая url_name атрибутом .basename.

Обратите внимание, что basename предоставляется маршрутизатором во время регистрации ViewSet. Если вы не используете маршрутизатор, то вы должны предоставить аргумент basename методу .as_view().

Используя пример из предыдущего раздела:

>>> view.reverse_action("set-password", args=["1"])
'http://localhost:8000/api/users/1/set_password'

В качестве альтернативы можно использовать атрибут url_name, установленный декоратором @action.

>>> view.reverse_action(view.set_password.url_name, args=['1'])
'http://localhost:8000/api/users/1/set_password'

Аргумент url_name для .reverse_action() должен совпадать с тем же аргументом декоратора @action. Кроме того, этот метод можно использовать для отмены действий по умолчанию, таких как list и create.


API Reference

ViewSet

Класс ViewSet наследуется от APIView. Вы можете использовать любые стандартные атрибуты, такие как permission_classes, authentication_classes, чтобы управлять политикой API для набора представлений.

Класс ViewSet не предоставляет никаких реализаций действий. Чтобы использовать класс ViewSet, вам нужно переопределить его и явно определить реализацию действий.

GenericViewSet

Класс GenericViewSet наследуется от GenericAPIView и предоставляет стандартный набор методов get_object, get_queryset и другие базовые поведения общих представлений, но по умолчанию не включает никаких действий.

Чтобы использовать класс GenericViewSet, вы должны переопределить его и либо смешать необходимые классы mixin, либо явно определить реализацию действий.

ModelViewSet

Класс ModelViewSet наследуется от GenericAPIView и включает в себя реализации различных действий, смешивая поведение различных классов-миксинов.

Класс ModelViewSet предоставляет следующие действия: .list(), .retrieve(), .create(), .update(), .partial_update() и .destroy().

Пример

Поскольку ModelViewSet расширяет GenericAPIView, вам обычно нужно предоставить как минимум атрибуты queryset и serializer_class. Например:

class AccountViewSet(viewsets.ModelViewSet):
    """
    A simple ViewSet for viewing and editing accounts.
    """
    queryset = Account.objects.all()
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]

Обратите внимание, что вы можете использовать любой из стандартных атрибутов или переопределений методов, предоставляемых GenericAPIView. Например, чтобы использовать ViewSet, который динамически определяет набор запросов, с которым он должен работать, вы можете сделать что-то вроде этого:

class AccountViewSet(viewsets.ModelViewSet):
    """
    A simple ViewSet for viewing and editing the accounts
    associated with the user.
    """
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]

    def get_queryset(self):
        return self.request.user.accounts.all()

Однако обратите внимание, что, после удаления свойства queryset из вашего ViewSet, любой связанный с ним router не сможет автоматически вывести базовое имя вашей Модели, поэтому вам придется указать именованный аргумент basename как часть вашей регистрации router.

Также обратите внимание, что хотя этот класс по умолчанию предоставляет полный набор действий create/list/retrieve/update/destroy, вы можете ограничить доступные операции с помощью стандартных классов разрешений.

ReadOnlyModelViewSet

Класс ReadOnlyModelViewSet также наследуется от GenericAPIView. Как и ModelViewSet, он также включает реализации различных действий, но в отличие от ModelViewSet предоставляет только действия "только для чтения", .list() и .retrieve().

Пример

Как и в случае с ModelViewSet, обычно вам нужно предоставить как минимум атрибуты queryset и serializer_class. Например:

class AccountViewSet(viewsets.ReadOnlyModelViewSet):
    """
    A simple ViewSet for viewing accounts.
    """
    queryset = Account.objects.all()
    serializer_class = AccountSerializer

Как и в случае с ModelViewSet, вы можете использовать любые стандартные атрибуты и переопределения методов, доступные для GenericAPIView.

Пользовательские базовые классы ViewSet

Вам может понадобиться предоставить пользовательские классы ViewSet, которые не имеют полного набора действий ModelViewSet или настраивают поведение каким-либо другим способом.

Пример

Чтобы создать базовый класс набора представлений, обеспечивающий операции create, list и retrieve, наследуйте от GenericViewSet и добавьте необходимые действия:

from rest_framework import mixins, viewsets

class CreateListRetrieveViewSet(mixins.CreateModelMixin,
                                mixins.ListModelMixin,
                                mixins.RetrieveModelMixin,
                                viewsets.GenericViewSet):
    """
    A viewset that provides `retrieve`, `create`, and `list` actions.

    To use it, override the class and set the `.queryset` and
    `.serializer_class` attributes.
    """
    pass

Создавая собственные базовые классы ViewSet, вы можете обеспечить общее поведение, которое может быть повторно использовано в нескольких наборах представлений в вашем API.

Last updated