# Viewsets

## ViewSets

> После того, как маршрутизация определила, какой контроллер использовать для запроса, ваш контроллер отвечает за осмысление запроса и создание соответствующего вывода.
>
> — [Ruby on Rails Documentation](https://guides.rubyonrails.org/action_controller_overview.html)

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

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

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

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

### Пример

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

```python
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)
```

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

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

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

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

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

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

```python
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, как показано ниже:

```python
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`, следующим образом:

```python
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.

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

```python
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`. Например:

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

Аргумент `methods` также поддерживает методы HTTP, определенные как [HTTPMethod](https://docs.python.org/3/library/http.html#http.HTTPMethod). Пример ниже идентичен приведенному выше:

```python
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`...:

```python
    @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`. Например, описанные выше методы установки и снятия пароля могут быть объединены в один маршрут. Обратите внимание, что дополнительные сопоставления не принимают аргументов.

```python
    @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()`.

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

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

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

```pycon
>>> 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`. Например:

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

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

```python
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](https://ilyachch.gitbook.io/django-rest-framework-russian-documentation/overview/navigaciya-po-api/routers) не сможет автоматически вывести базовое имя вашей Модели, поэтому вам придется указать именованный аргумент `basename` как часть вашей регистрации [router](https://ilyachch.gitbook.io/django-rest-framework-russian-documentation/overview/navigaciya-po-api/routers).

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

### ReadOnlyModelViewSet

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

**Пример**

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

```python
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` и добавьте необходимые действия:

```python
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.
