# ViewSet

Django REST framework는 `ViewSet` 이라는 단일 클래스에서 관련된 논리를 결합할 수 있다. `ViewSet`은 단순히 `get()`이나 `post()`와 같은 메소드 핸들러를 제공하지 않으며, 대신 `list()` 와 `create()` 같은 액션을 제공한다(CBV 유형). `ViewSet` 의 method 핸들러는 **`as_view()` 함수가 호출되어 view가 끝나는 시점의 해당 액션에만 바인딩**된다.

> 바인딩(binding)
>
> **각종 값들이 확정되어 더 이상 변경할 수 없는 상태가 되는 것**이다. 식별자(identifier)가 그 대상인 메모리 주소, 데이터형 또는 실제값으로 배정되는 것을 뜻한다.

일반적으로 url설정시에 view를 명시적으로 등록하는 대신 **`router` 클래스로 등록하여 자동으로 url을 설정**한다.

## START VIEWSETS

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

class UserViewSet(viewsets.ViewSet):
    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)
```

필요시에는 이 ViewSet을 아래와 같이 개별 뷰 바인딩을 할 수 있다.

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

일반적으로 개별 뷰 바인딩을 하지 않으며, ViewSet을 라우터에 등록하여 자동으로 URLconf(url 설정파일)이 생성되도록 한다.

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

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

이때 viewset을 본인이 작성하는 것보다 **기본 동작을 제공해주는 기본 클래스를 사용**하는 것을 권장한다.

```python
class UserViewSet(viewsets.ModelViewSet):
  serializer_class = UserSerializer
  queryset = User.objects.all()
```

### 장점

view 클래스를 사용하는 것보다 viewsets 클래스를 사용하면 좋은 점이 2가지 있다.

1. 반복되는 로직을 하나의 클래스로 결합할 수 있다. 즉, viewsets을 이용하면 **queryset을 단 한번만 정의**하면 된다.
2. **router를 사용**함으로써, URLconf(url 설정파일)을 다룰 필요가 없어진다.

하지만 아래와 같은 trade-off도 있다.

* view와 url설정을 사용하면 보다 명확하고 상세하게 제어할 수 있다.

`ViewSet` 은 빠르게 실행하거나 대규모 API가 있는 경우, 또는 전체적으로 일관된 URLconf를 적용하려는 경우에 유리하다.

## MARKING EXTRA ACTIONS FOR ROUTING

REST framework에 포함된 기본 router는 **create** / **retrieve** / **update** / **destroy** 스타일 작업의 기본 method를 제공한다.

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

라우팅이 필요한 임시 메소드가 있는 경우에는 `@detail_router` 또는 `@list_router` 데코레이터를 사용하여 라우팅을 필요로 한다는 것을 표시할 수 있다.

* `@detail_router` 는 URL 패턴에 **pk**를 포함하며 단일 인스턴스를 요구하는 메소드에 대한 것이다.
* `@list_router` 는 객체 목록에서 작동하는 메소드에 대한 것이다.

```python
rom django.contrib.auth.models import User
from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from .serializers import UserSerializer, PasswordSerializer

class UserViewSet(viewsets.ModelViewSet):

    queryset = User.objects.all()
    serializer_class = UserSerializer

    @detail_route(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.data['password'])
            user.save()
            return Response({'status': 'password set'})
        else:
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    @list_route()
    def recent_users(self, request):
        recent_users = User.objects.all().order('-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)
```

데코레이터는 오직 라우팅된 View에서만 추가적인 arguments를 받을 수 있다.

```python
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
  ...
```

해당 데코레이터는 기본적으로 `GET` request를 routing하지만 `methods` 인수를 사용하여 다른 HTTP Method로 routing할 수 있다.

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

위의 두가지 새로운 action들은 `^users/{pk}/set_password/$`, `^users/{pk}/unset_password/$` 로 사용할 수 있다.

## API REFERENCE

### ViewSet

`ViewSet` 클래스는 `APIView` 에서 상속받는다. `permission_classes`, `authentication_classes` 와 같은 표준 속성을 이용하여 API 정책을 통제할 수 있다.

`ViewSet` 클래스는 action의 구현을 제공하지 않는다. `ViewSet` 클래스를 사용하기 위해서는 해당 클래스를 overrice하고, 명시적으로 action을 정의해야한다.

### GenericViewSet

`GenericViewSet` 클래스는 `GenericAPIView` 에서 상속 받는다. `get_object`, `get_queryset` 메소드와 다른 generic view의 기본 action을 제공하지만, 기본적으로는 어떠한 액션도 포함하고 있지 않다.

`GenericViewSet` 클래스를 사용하기 위해서는 해당 클래스를 override하고 필요한 `mixin` 클래스를 혼합하여 명시적으로 action을 정의해아한다.

### ModelViewSet

`ModelViewSet` 클래스는 `GenericAPIView` 를 상속하고 다수의 `mixin` 클래스의 동작을 혼합하여 다양한 action에 대한 구현을 포함한다.

`.list()`, `.retrieve()`, `.create()`, `.update()`, `.partial_update()`, `.destroy()` 를 제공한다.

```python
class AccountViewSet(viewsets.ModelViewSet):
    queryset = Account.objects.all()
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]
```

`GenericAPIView` 를 상속받고 있기 대문에 기본적으로 `queryset`과 `serializer_class` 속성을 필수로 제공해야한다.

```python
class AccountViewSet(viewsets.ModelViewSet):
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]

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

queryset을 동적으로 결정하는 viewsets을 사용하려면 위와 같이 메소드 오버라이드를 할 수 있다.

여기서 주의해야할 점이 있다. `ViewSet`의 `queryset` 속성을 제거하면 연결된 라우터가 모델의 **base\_name**을 자동으로 파생시킬 수 없으므로 **base\_name**의 **kwarg**를 지정해야한다. 이 클래스는 기본적으로 **create** / **list** / **retrieve** / **update** /**destroy** 액션을 제공하지만 표준 권한 클래스(permission)를 사용해 제한할 수 있다.

### ReadOnlyModelViewSet

`ReadOnlyModelViewSet` 클래스는 `GenericAPIView` 를 상속한다. `ModelViewSet`과 동일하게 다양한 action애 대한 구현도 포함되지만 오직 읽기 전용 action인 `.list()` 와 `.retrieve()`만 제공한다.

```python
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Account.objects.all()
    serializer_class = AccountSerializer
```

## 참고링크

* <https://kimdoky.github.io/django/2018/07/07/drf-ViewSets.html>
* <https://brownbears.tistory.com/82>
