Skip to content

Latest commit

 

History

History
824 lines (602 loc) · 29.7 KB

lesson30.md

File metadata and controls

824 lines (602 loc) · 29.7 KB

Лекция 30. @api_view, APIView, ViewSets, Pagination, Routers

Все мы помним, что веб в первую очередь - это Request-Response система.

Request

Что нового в request.

Дока тут

Два новых парамера .data и .query_string

.data - данные, если запрос POST, PUT или PATCH, аналог request.POST или request.FILES

.query_string - данные, если запрос GET, аналог request.GET

И параметры .auth и .authenticate, которые мы рассмотрим на следующей лекции. Она целиком про авторизацию и permissions (доступы).

Response

Дока тут

В отличие от классической Django, ответом в REST системе будет обычный HTTP-ответ, содержащий набор данных, чаще всего JSON (но бывает и нет).

Классическая Django тоже может возвращать HTTP-ответ и быть обработчиком REST архитектуры, но существующий пакет сильно упрощает эти процессы.

Для обработки такого ответа есть специальный объект:

Response(data, status=None, template_name=None, headers=None, content_type=None)

где data - данные,

status - код ответа (200, 404, 503),

template_name - возможность указать темплейт, если необходимо вернуть страницу, а не просто набор данных,

headers и content_type - заголовки и тип содержимого запроса.

Настройка для получения JSON

Если нужно указать явно формат для получения или отправки, то можно указать его в settings.py:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    'DEFAULT_PARSER_CLASSES': [
        'rest_framework.parsers.JSONParser',
    ]
}

@api_view

Дока тут

Для описания endpoint функционально нужно указать декоратор api_view и методы, которые он может принимать. Возвращает всё также объект ответа. Для использования возьмем модель Book и сериалайзер BookSerializer, из последнего примера прошлой лекции

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from myapp.models import Book
from myapp.serializers import BookSerializer


@api_view(['GET', 'POST'])
def book_list(request):
    """
    List all books, or create a new book.
    """
    if request.method == 'GET':
        books = Book.objects.all()
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        book = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

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

Если попытаться получить доступ методом, который не разрешен, запрос будет отклонён с ответом 405 Method not allowed

Для передачи параметров используются аргументы функции. (Очень похоже на обычную Django вью)

@api_view(['GET', 'PUT', 'DELETE'])
def book_detail(request, pk):
    """
    Retrieve, update or delete a book.
    """
    try:
        book = Book.objects.get(pk=pk)
    except Book.DoesNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = BookSerializer(book)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = BookSerializer(book, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        book.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

URLs для таких методов описываются точно так же как и для стандартной Django вью.

from myapp.view import book_list, book_detail

urlpatterns = [
    path('books/', book_list),
    path('books/<int:pk>/', book_detail),
]

Ответ на GET-запрос в этому случае будет выглядеть так:

[
  {
      "title": "Harry Potter and the Philosopher's Stone",
      "published_date": "1997-06-26",
      "id": 1
    },
    {
      "title": "Harry Potter and the Chamber of Secrets",
      "published_date": "1998-07-02",
      "id": 2
    }
]

Поля будут зависеть от модели и сериалайзера соответственно.

Ответ на POST запрос (создание объекта):

{
   "title": "test title",
   "published_date": "1998-07-02",
   "id": 3
}

View

Знакомимся с самым подробным сайтом по DRF классам тут

APIView

Дока тут

Также мы можем описать это же через Class-Based View, для этого нам нужно наследоваться от APIView:

from myapp.models import Book
from myapp.serializers import BookSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class BookList(APIView):
    """
    List all books, or create a new book.
    """

    def get(self, request, format=None):
        books = Book.objects.all()
        serializer = BookSerializer(books, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Вынесем получение объекта в отдельный метод:

class BookDetail(APIView):
    """
    Retrieve, update or delete a book instance.
    """

    def get_object(self, pk):
        try:
            return Book.objects.get(pk=pk)
        except Book.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        book = self.get_object(pk)
        serializer = BookSerializer(book)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        book = self.get_object(pk)
        serializer = BookSerializer(book, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        book = self.get_object(pk)
        book.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

URLs описываются так же, как и для Django Class-Based View:

urlpatterns = [
    path('books/', BookList.as_view()),
    path('books/<int:pk>/', BookDetail.as_view()),
]

GenericView

По аналогии с классической Django существуют заранее описанные CRUD действия.

Как это работает?

Существует класс GenericAPIView, который наследуется от обычного APIView.

В нём описаны такие поля как:

  • queryset хранит кверисет;

  • serializer_class хранит сериалайзер;

  • lookup_field = 'pk' - название атрибута в модели, который будет отвечать за PK;

  • lookup_url_kwarg = None - название атрибута в запросе, который будет отвечать за pk;

  • filter_backends = api_settings.DEFAULT_FILTER_BACKENDS - фильтры запросов;

  • pagination_class = api_settings.DEFAULT_PAGINATION_CLASS - пагинация запросов.

И методы:

  • get_queryset - получение кверисета;

  • get_object - получение одного объекта;

  • get_serializer - получение объекта сериалайзера;

  • get_serializer_class - получение класса сериалайзера;

  • get_serializer_context - получить контекст сериалайзера;

  • filter_queryset - отфильтровать кверисет;

  • paginator - объект пагинации;

  • paginate_queryset - пагинировать кверисет;

  • get_paginated_response - получить пагинированый ответ.

Такой класс не работает самостоятельно, только вместе с определёнными миксинами

Миксины

В DRF существует 5 миксинов:

class CreateModelMixin(object):
    """
    Create a model instance.
    """

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

Рассмотрим подробнее.

Это миксин, и без сторонних классов этот функционал работать не будет.

При вызове метода create() мы предполагаем, что у нас был request.

Вызываем метод get_serializer() из класса GenericAPIView для получения объекта сериалайзера, обратите внимание, что данные передаются через атрибут data, так как они получены от пользователя. Проверяем данные на валидность (обратите внимание на атрибут raise_exception, если данные будут не валидны, код сразу вылетит в traceback, а значит нам не нужно отдельно прописывать действия при не валидном сериалайзере), вызываем метод perform_create, который просто сохраняет сериалайзер (вызывает create или update в зависимости от данных), получает хедеры, и возвращает response с 201 кодом, создание успешно.

По аналогии мы можем рассмотреть остальные миксины.

class RetrieveModelMixin(object):
    """
    Retrieve a model instance.
    """

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

Обратите внимание, метод называется retrieve() и внутри вызывает метод get_object(), - это миксин одиночного объекта

class UpdateModelMixin(object):
    """
    Update a model instance.
    """

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

    def partial_update(self, request, *args, **kwargs):
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

Методы update(), partial_update(), perform_update() нужны для обновления объекта, обратите внимание на атрибут partial. Помните разницу между PUT и PATCH?

class DestroyModelMixin(object):
    """
    Destroy a model instance.
    """

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)

    def perform_destroy(self, instance):
        instance.delete()

Аналогично для удаления методы destroy(), perform_destroy().

class ListModelMixin(object):
    """
    List a queryset.
    """

    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

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

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

Метод list() получает кверисет, дальше пытается его пагинировать, если получается, возвращает страницу, если нет - целый ответ.

Важно! Ни в одном из миксинов не было методов get(),post(),patch(),put() или delete(), почему?

Потому что их вызов перенесен в дополнительные классы.

Generic классы

Вот так выглядят классы, которые уже можно использовать. Как это работает? Эти классы наследуют логику работы с данными из необходимого миксина, общие методы, которые актуальны для любого CRUD действия из GenericAPIView дальше описываем методы тех видов запросов, которые мы хотим обрабатывать, в которых просто вызываем необходимый метод из миксина.

Переписываются методы create(), destroy() и т. д., а не get(), post()!

class CreateAPIView(mixins.CreateModelMixin,
                    GenericAPIView):
    """
    Concrete view for creating a model instance.
    """

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)
class ListAPIView(mixins.ListModelMixin,
                  GenericAPIView):
    """
    Concrete view for listing a queryset.
    """

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)
class RetrieveAPIView(mixins.RetrieveModelMixin,
                      GenericAPIView):
    """
    Concrete view for retrieving a model instance.
    """

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)
class DestroyAPIView(mixins.DestroyModelMixin,
                     GenericAPIView):
    """
    Concrete view for deleting a model instance.
    """

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)
class UpdateAPIView(mixins.UpdateModelMixin,
                    GenericAPIView):
    """
    Concrete view for updating a model instance.
    """

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)
class ListCreateAPIView(mixins.ListModelMixin,
                        mixins.CreateModelMixin,
                        GenericAPIView):
    """
    Concrete view for listing a queryset or creating a model instance.
    """

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)
class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
                            mixins.UpdateModelMixin,
                            GenericAPIView):
    """
    Concrete view for retrieving, updating a model instance.
    """

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)
class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
                             mixins.DestroyModelMixin,
                             GenericAPIView):
    """
    Concrete view for retrieving or deleting a model instance.
    """

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)
class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
                                   mixins.UpdateModelMixin,
                                   mixins.DestroyModelMixin,
                                   GenericAPIView):
    """
    Concrete view for retrieving, updating or deleting a model instance.
    """

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def patch(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

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

class CommentListView(ListCreateAPIView):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer

    def get_queryset(self):
        return super().get_queryset().filter(text__icontains='w')

Всё, этого достаточно.

ViewSet

Дока тут

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

Они на самом деле описывают методы, для получения списка действий (list, retrieve, и т. д.), и преобразования их в URLs (об этом дальше).

Например:

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)

Разные действия при наличии и отсутствии PK, при GET запросе.

Для описания URLs можно использовать разное описание:

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

Хоть так никто и не делает, об этом дальше.

ModelViewSet и ReadOnlyModelViewSet

Дока тут

Объединяем всё, что мы уже знаем.

И получаем класс ModelViewSet, он наследуется от GenericViewSet (ViewSet + GenericAPIView) и всех 5 миксинов, а значит там описаны методы list(), retrieve(), create(), update(), destroy(), perform_create(), perform_update() и т. д.

А значит мы можем описать сущность, которая принимает модель и сериалайзер, и уже может принимать любые типы запросов и выполнять любые CRUD действия. Мы можем их переопределить, или дописать еще экшенов, всё что нам может быть необходимо уже есть.

Пример:

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

Или если необходим такой же вьюсет только для получения объектов, то:

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

На самом деле, можно собрать такой же вьюсет для любых действий, добавляя и убирая миксины.

Например:

from rest_framework import mixins


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

Чаще всего используются обычные ModelViewSet.

Пагинация

Дока тут

Как мы помним, для действия list используется пагинация. Как это работает?

Если у нас нет необходимости настраивать все вьюсеты отдельно, то мы можем указать такую настройку в settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 100
}

Там мы можем указать тип класса пагинации и размер одной страницы, и все наши запросы уже будут пагинированы.

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

class LargeResultsSetPagination(PageNumberPagination):
    page_size = 1000
    page_size_query_param = 'page_size'
    max_page_size = 10000


class StandardResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000

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

class BillingRecordsView(generics.ListAPIView):
    queryset = Billing.objects.all()
    serializer_class = BillingRecordsSerializer
    pagination_class = LargeResultsSetPagination

Декоратор @action

Дока тут

Что делать, если вам нужно дополнительное действие, связанное с деталями вашей вью, но ни один из крудов не походит? Тут можно использовать декоратор @action, чтобы описать новое действие в этом же вьюсете.

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.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 = self.get_queryset().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)

Принимает два основных параметра: detail описывает должен ли этот action принимать PK (действие над всеми объектами или над одним конкретным), и methods - список HTTP методов, на которые должен срабатывать action.

Есть и другие, например, классы permissions или имя.

Роутеры

Дока тут

Роуте - это автоматический генератор URLs для вьюсетов.

from rest_framework import routers

router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
urlpatterns = router.urls

В методе register принимает два параметра, на каком слове основывать URLs и для какого вьюсета.

Если у вьюсета нет параметра queryset, то нужно указать поле basename, если нет, то автоматически будет использовано имя модели маленькими буквами.

URLs будут сгенерированы автоматически, и им будут автоматически присвоены имена:

URL pattern: ^users/$ Name: 'user-list'
URL pattern: ^users/{pk}/$ Name: 'user-detail'
URL pattern: ^accounts/$ Name: 'account-list'
URL pattern: ^accounts/{pk}/$ Name: 'account-detail'

Чаще всего роутеры к URLs добавляются вот такими способами:

urlpatterns = [
    path('forgot-password', ForgotPasswordFormView.as_view()),
    path('api/', include(router.urls)),
]

Роутинг экстра экшенов

Допустим, есть такой экстра экшен:

from rest_framework.decorators import action


class UserViewSet(ModelViewSet):
    ...

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

Роутер автоматически сгенерирует URL ^users/{pk}/set_password/$ и имя user-set-password.

Класс SimpleRouter может принимать параметр trailing_slash=False True или False, по дефолту True, поэтому все API, должны принимать URLs заканчивающиеся на /, если указать явно, то будет принимать всё без /.