Django REST framework

Введение

DRF или Django REST framework - библиотека Django для написания Web APIs. Для того чтобы лучше понять зачем это вообще нужно давайте вспомним как устроена работа Django.

Первым делом мы вводим интересующий нас url адрес, в файле urls.py django ищет адрес, соответствующий переданному, если находит, то обращается ко view связанному с этим url, если нет - возвращает 404. В случае, когда url найден, а значит и найден его view, происходит запрос к БД от этого view, в ответ на который возвращаются какие-то данные и передаются в шаблон, связанный с этим view. Внутри шаблона эти данные подставляются в отведенные для них, с помощью template тегов, места и уже эта заполненная нужными данными html страница возвращается браузеру, который показывает ее пользователю. Во всем этом процессе еще присутствуют Middleware, но ранее мы их не касались, так что опустим их в этой схеме, на понимание это никак не влияет.

Примерно так выглядит весь цикл Django. Но когда речь идет о взаимодействии между приложениями и различными итоговыми устройствами, обрабатывающими эти приложения, например, смартфонами, мы пользуемся протоколом REST. REST (Representational State Transfer — «передача репрезентативного состояния») ничто иное как описание этого самого взаимодействия. Приложение, запрашивающее данные предпочитает получать их в json или xml формате и с помощью DRF мы пишем API для преобразования, отдаваемых нашим приложением данных, именно в эти предпочтительные форматы, чаще, конечно, json.

Таким образом, с использованием DRF работу Django теперь можно описать следующим образом. До обращения ко view схема никак не изменяется, правда view, когда речь идет о DRF уже немного видоизмененные. А вот между view и БД появляется 'прослойка', в которой и случается вся магия DRF. Называется эта прослойка - Сериализатор. Этот самый сериализатор преобразует данные из БД, которые являются объектами python, в формат json и наоборот, если нужно через эту view внести изменения в БД, то сериализатор преобразует полученные в формате json данные к формату python и записывает их в БД.

Официальная документация DRF

А теперь начнем разбираться во всем подробнее.

Установка DRF. Сериализаторы

Для обучения DRF будем использовать сайт, который мы писали в блоке 'Django. Углубление', оставлю ссылку на коммит, можете склонировать его, правда БД придется заполнить самостоятельно, но думаю большого труда это не составит.

Перед использованием DRF его разумеется нужно установить. Делается это командой

pip install djangorestframework

После установки его нужно зарегистрировать в settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
]


И добавить путь приложения в urls.py
urlpatterns = [
    ...
    path('api-auth/', include('rest_framework.urls'))
]

Все, DRF готов к использованию. Можем перейти к написанию первого сериализатора.

Первый вопрос, который возникает. Где писать эти самые сериализаторы?

В нашем проекте всего одно приложение, мы можем писать api проекта в этом приложении, а можем вынести все api в отдельное, так в случае масштабирования проекта мы просто будем расширять приложение с нашим api. Поступить можно как угодно, я предпочту создать новое приложение в проекте и писать api там. Пусть приложение так и называется - api.

python manage.py startapp api

INSTALLED_APPS = [
    ...
    'api',
]

Пишутся сериализаторы в отдельном файле с названием serializers.py, создадим такой файл в приложении api.

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

Напишем в serializers.py следующее.

api/serializers.py
from rest_framework import serializers
from book.models import Author


class AuthorSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(required=True, max_length=150)
    country = serializers.CharField(required=False, allow_blank=True, max_length=150)

    def create(self, validated_data):
        return Author.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.name = validated_data.get('name', instance.name)
        instance.country = validated_data.get('country', instance.country)
        instance.save()
        return instance

Сначала разберемся с сериализатором унаследованным от класса Serializer. Сериализатор должен содержать поля, совпадающими с полями модели, для которой этот сериализатор написан. Название основных типов полей сериализаторов совпадает с типами полей моделей, со всеми типами можно ознакомится на этой странице документации. Имена параметров же немного отличаются от знакомых нам.

Поле id необходимо учитывать при написании сериализаторов, не забывайте о нем, параметр read_only задает режим взаимодействия с полем, id у нас заполняется автоматически, но пропускать через сериализатор его все-равно необходимо.
Поля name и country обычные текстовые поля. Параметр required, в зависимости от значения, говорит должно ли поле быть заполнено. allow_blank позволяет полю быть пустым. Для использования в поле значения None используется параметр allow_null.

Метод create() (POST) для создания нового экземпляра класса, то есть создания новой записи в БД, используется знакомый из ORM метод .create(), validated_data - словарь переданных для занесения в БД данных.

Метод update() (PUT) для изменения значений и занесения значений в нужные поля. Параметр instance содержит набор полей из модели, поскольку является ее экземпляром. Через instance мы записываем в БД переданные в словарь данные, подставляя их в нужные поля.

Теперь перейдем в консоль и посмотрим, что можно сделать с помощью этого сериализатора.

shell
>>> from book.models import Author
>>> from api.serializers import AuthorSerializer
>>> from rest_framework.renderers import JSONRenderer
>>> from rest_framework.parsers import JSONParser

>>> authors = AuthorSerializer(Author.objects.all(), many=True)
>>> authors.data
[OrderedDict([('id', 8), ('name', 'Адитья Бхаргава'), ('country', 'Россия')]), OrderedDict([('id', 7), ('name', 'Скотт Чакон'), ('country', 'Россия')]), OrderedDict([('id', 6), ('name', 'Бен Страуба'), ('country', 'Россия')]), OrderedDict([('id', 5), ('name', 'Евгений Моргунов'), ('country', 'Россия')]), OrderedDict([('id', 4), ('name', 'Александр Иванович Герцен'), ('country', 'Россия')]), OrderedDict([('id', 3), ('name', 'Уильям Берроуз'), ('country', 'Россия')]), OrderedDict([('id', 2), ('name', 'Александр Шульгин'), ('country', 'Россия')]), OrderedDict([('id', 1), ('name', 'Энн Шульгина'), ('country', 'Россия')])]

>>> serializers_authors = JSONRenderer().render(authors.data)
>>> serializers_authors
b'[{"id":8,"name":"\xd0\x90\xd0\xb4\xd0\xb8\xd1\x82\xd1\x8c\xd1\x8f \xd0\x91\xd1\x85\xd0\xb0\xd1\x80\xd0\xb3\xd0\xb0\xd0\xb2\xd0\xb0","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":7,"name":"\xd0\xa1\xd0\xba\xd0\xbe\xd1\x82\xd1\x82 \xd0\xa7\xd0\xb0\xd0\xba\xd0\xbe\xd0\xbd","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":6,"name":"\xd0\x91\xd0\xb5\xd0\xbd \xd0\xa1\xd1\x82\xd1\x80\xd0\xb0\xd1\x83\xd0\xb1\xd0\xb0","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":5,"name":"\xd0\x95\xd0\xb2\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xb8\xd0\xb9 \xd0\x9c\xd0\xbe\xd1\x80\xd0\xb3\xd1\x83\xd0\xbd\xd0\xbe\xd0\xb2","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":4,"name":"\xd0\x90\xd0\xbb\xd0\xb5\xd0\xba\xd1\x81\xd0\xb0\xd0\xbd\xd0\xb4\xd1\x80 \xd0\x98\xd0\xb2\xd0\xb0\xd0\xbd\xd0\xbe\xd0\xb2\xd0\xb8\xd1\x87 \xd0\x93\xd0\xb5\xd1\x80\xd1\x86\xd0\xb5\xd0\xbd","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":3,"name":"\xd0\xa3\xd0\xb8\xd0\xbb\xd1\x8c\xd1\x8f\xd0\xbc \xd0\x91\xd0\xb5\xd1\x80\xd1\x80\xd0\xbe\xd1\x83\xd0\xb7","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":2,"name":"\xd0\x90\xd0\xbb\xd0\xb5\xd0\xba\xd1\x81\xd0\xb0\xd0\xbd\xd0\xb4\xd1\x80 \xd0\xa8\xd1\x83\xd0\xbb\xd1\x8c\xd0\xb3\xd0\xb8\xd0\xbd","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"},{"id":1,"name":"\xd0\xad\xd0\xbd\xd0\xbd \xd0\xa8\xd1\x83\xd0\xbb\xd1\x8c\xd0\xb3\xd0\xb8\xd0\xbd\xd0\xb0","country":"\xd0\xa0\xd0\xbe\xd1\x81\xd1\x81\xd0\xb8\xd1\x8f"}]'

Если просто взять все записи из модели через наш сериализатор, то на выходе мы получим OrderedDict(), а вот уже для преобразования этого словаря к байтовому json формату используется метод render() класса JSONRenderer(), куда мы передаем этот самый OrderedDict. Параметр many=True используется, когда мы забираем больше чем одну запись из БД.

Содержимое переменной serializers_authors содержит данные в json формате, которые из json можно распрасить обратно в необходимые форматы в зависимости от языка, который эти данные запрашивал.

Теперь десериализируем эти данные.

shell
>>> import io

>>> return_authors = io.BytesIO(serializers_authors)
>>> data = JSONParser().parse(return_authors)
>>> data
[{'id': 8, 'name': 'Адитья Бхаргава', 'country': 'Россия'}, {'id': 7, 'name': 'Скотт Чакон', 'country': 'Россия'}, {'id': 6, 'name': 'Бен Страуба', 'country': 'Россия'}, {'id': 5, 'name': 'Евгений Моргунов', 'country': 'Россия'}, {'id': 4, 'name': 'Александр Иванович Герцен', 'country': 'Россия'}, {'id': 3, 'name': 'Уильям Берроуз', 'country': 'Россия'}, {'id': 2, 'name': 'Александр Шульгин', 'country': 'Россия'}, {'id': 1, 'name': 'Энн Шульгина', 'country': 'Россия'}]

Для этого воспользуемся классом BytesIO() из библиотеки io, для преобразования байтовых данных. И методом parse() класса JSONParser() для преобразования их к json словарю.

shell
>>> new_author = Author(name='some_author', country='some_country')
>>> new_author.save()

>>> serializer = AuthorSerializer(new_author)
>>> serializer.data
{'id': 9, 'name': 'some_author', 'country': 'some_country'}

>>> content = JSONRenderer().render(serializer.data)
>>> content
b'{"id":9,"name":"some_author","country":"some_country"}'

>>> import io

>>> stream = io.BytesIO(content)
>>> data = JSONParser().parse(stream)
>>> serializer = AuthorSerializer(data=data)
>>> serializer.is_valid()
True

>>> serializer.validated_data
OrderedDict([('name', 'some_author'), ('country', 'some_country')])

>>> serializer.save()
<Author: some_author>

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

api/serializers.py
from rest_framework.serializers import ModelSerializer
from book.models import Author


# class AuthorSerializer(serializers.Serializer):
#     id = serializers.IntegerField(read_only=True)
#     name = serializers.CharField(required=False, max_length=150)
#     country = serializers.CharField(required=True, allow_blank=True, max_length=150)
#
#     def create(self, validated_data):
#         return Author.objects.create(**validated_data)
#
#     def update(self, instance, validated_data):
#         instance.name = validated_data.get('name', instance.name)
#         instance.country = validated_data.get('country', instance.country)
#         instance.save()
#         return instance

class AuthorSerializer(ModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name', 'country']

А теперь, вместо сериализатора, написанного выше, мы наследуемся от ModelSerializer. В классе Meta пишем модель и используемые поля. Вернемся в shell и посмотрим, что содержит в себе AuthorSerializer.

shell
>>> from api.serializers import AuthorSerializer
>>> serializer = AuthorSerializer()
>>> print(repr(serializer))
AuthorSerializer():
    id = IntegerField(label='ID', read_only=True)
    name = CharField(max_length=250)
    country = CharField(allow_blank=True, allow_null=True, max_length=150, required=False)

Импортируем сериализатор, создадим его экземпляр и выведем на печать. Получаем то, что в первом варианте сериализатора мы писали вручную. Уже гораздо проще и компактнее, а все необходимые методы ModelSerializer уже содержит.

@api_view

Смотреть в консоли как запись из БД меняется в json и обратно, конечно, нужно для понимания идеи, но все-таки хотелось бы взглянуть на результат со стороны пользователя, который будет взаимодействовать с вашим web-приложением через ваше api. И для представления сериализаторов мы традиционно используем view. Разумеется, не стандартный view, а view из DRF.

Как и в случае со стандартным django представления мы можем создавать на базе функций и классов. Начнем с view на базе функций.

api/views.py
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework import status

from book.models import Author
from api.serializers import AuthorSerializer


@api_view(["GET", "POST"])
def authors_list(request):
    if request.method == "GET":
        authors = Author.objects.all()
        serializers = AuthorSerializer(authors, many=True)
        return Response(serializers.data, status=status.HTTP_200_OK)
    elif request.method == "POST":
        serializer = AuthorSerializer(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)

Для представлений на базе функций используется декоратор @api_view из rest_framework.decorators. В этот декоратор следует обернуть нашу функцию. Напишем функцию для вывода авторов. @api_view в качестве параметров принимает http методы, так добавим методы GET и POST. Функция традиционно должна принимать как минимум один обязательный параметр - request. Далее внутри функции мы делаем проверку метода. В случе GET будем забирать все записи из модели Author и пропускать их через сериализатор, написанный нами выше, примерно то же самое мы делали, когда работали с сериализаторами в shell. Параметр many=True, поскольку данная модель содержит больше чем одну запись.

Класс Response() необходим для создания объекта ответа на запрос. В этом классе уже реализованы JSONRenderer() и JSONParser(), то есть мы также передаем в этот класс данные из БД типа OrderedDict, используя для этого метод .data, а финальное преобразование к байтовому типу берет на себя класс Response(). Помимо data, Response() ожидает получить статус ответа. В блоке 'Django и интернет' мы разбирались с HTTP ответами, тут ситуация такая же, только вместо ответов мы возвращаем статусы, которые по значению полностью соответствуют HTTP ответам. Так ответ 200 и статус 200 свидетельствуют, что запрос выполнен успешно. Параметр status для класса Response является необязательным.

С методом POST ситуация обратная. Метод .data, применяемый к request, когда речь идет о DRF уже содержит данные, готовые к пропусканию через сериализатор. many=True нам уже не нужен, поскольку метод POST подразумевает добавление одной записи. Метод .is_valid() проверяет валидность переданных данных для записи их в соответствующие поля модели. Метод .save() для записи валидных записей в БД. Метод save() умеет сохранять запись в БД, потому что в сериализаторе есть метод create(), который мы вручную прописывали в первом сериализаторе, и который есть в сериалиазторе по умолчанию, когда мы наследуемся от Serializer и его дочерних классов. И в Response мы возвращаем добавленную запись и статус 201, говорящий об успешном добавлении записи. Если данные не валидны, возвращаем соответствующую ошибку, которые уже есть в методе .errors и статус 400.

pylibrary/urls.py
...
urlpatterns = [
    ...
    path('api/v1/', include('api.urls')),
    ...
]
...
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', authors_list)
]

Осталось связать представление с адресом. Все приложение api подключим в главном urls.py, с помощью функции include(), обычно путь для api так и называют - 'api/v(номер версии)'. В приложении api создадим файл urls.py с содержанием из примера.

Теперь можно запустить сервер и посмотреть, что возвращает данный адрес.

Мы получили все записи из модели Author в json формате, тут мы видим, используемый метод и поддерживаемые методы, адрес, статус ответа. Такой интерфейс как на скриншоте предоставляется сразу с библиотекой DRF. Для того чтобы увидеть 'голые' данные, в правом верхнем углу около метода, выберете json, вместо api, которое выбрано по умолчанию.

Выбрав формат json, мы видим чистые данные без стандартного интерфейса drf.

Мы написали функцию для вывода всех записей, формат - list. Давайте теперь напишем функцию для работы с одной записью, формат - detail.

api/views.py
...
@api_view(["GET", "PUT", "DELETE"])
def author_detail(request, pk):
    try:
        author = Author.objects.get(pk=pk)
    except:
        return Response(status=status.HTTP_404_NOT_FOUND)
    if request.method == "GET":
        serializer = AuthorSerializer(author)
        return Response(serializer.data, status=status.HTTP_200_OK)
    elif request.method == "PUT":
        serializer = AuthorSerializer(author, 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)
    elif request.method == "DELETE":
        author.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', authors_list),
    path('authors/<int:pk>', author_detail)
]

Продолжим делать это для модели Author. Детализация подразумевает отображение, изменение и удаление, используя для этого методы "GET", "POST" и "DELETE" соответственно. Но перед написанием логики каждого метода сделаем проверку переданного pk. Функция author_detail принимает pk, поскольку в этой функции мы работаем над конкретной записью, поэтому нам надо учесть, что в функцию может быть передан несуществующий pk. В блоке try except проверим есть ли запись с переданным pk, если да - возвратим эту запись, если нет - возвратим статус 404.

Реализация всех методов достаточно очевидна и в целом, мы со всем уже знакомы. В GET сериализуем, выбранную в зависимости от переданного pk, запись и возвращаем ее. В PUT мы предполагаем редактирование данных, поэтому забираем данные из request методом .data и говорим сериализатору, что для выбранной записи из Author подразумевается изменение данных на данные, хранящиеся в request.data. После этого проверяем валидность данных методом .is_valid() и в случае успешной проверки сохраняем изменения для этой записи. В DELETE мы удаляем запись и возвращаем статус 204.

В urls.py необходимо добавить путь для новой функции.

Перейдем на какую-нибудь запись и увидим интерфейс детализации конкретной записи.

Как видно, пишутся функции представления достаточно просто.

APIView

Давайте признаем, что для представлений мы чаще будем использовать именно классы, в классах уже реализовано то, что в функциях мы пишем вручную. Все классы представления в DRF наследуются от класса APIView. Для знакомства с ним заменим, написанные выше, функции на классы.

api/views.py
# from rest_framework.decorators import api_view
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

from book.models import Author
from api.serializers import AuthorSerializer


# @api_view(["GET", "POST"])
# def authors_list(request):
#     if request.method == "GET":
#         authors = Author.objects.all()
#         serializers = AuthorSerializer(authors, many=True)
#         return Response(serializers.data, status=status.HTTP_200_OK)
#     elif request.method == "POST":
#         serializer = AuthorSerializer(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 AuthorsListView(APIView):
    def get(self, request):
        authors = Author.objects.all()
        serializers = AuthorSerializer(authors, many=True)
        return Response(serializers.data, status=status.HTTP_200_OK)

    def post(self, request):
        serializer = AuthorSerializer(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)


# @api_view(["GET", "PUT", "DELETE"])
# def author_detail(request, pk):
#     try:
#         author = Author.objects.get(pk=pk)
#     except:
#         return Response(status=status.HTTP_404_NOT_FOUND)
#     if request.method == "GET":
#         serializer = AuthorSerializer(author)
#         return Response(serializer.data, status=status.HTTP_200_OK)
#     elif request.method == "PUT":
#         serializer = AuthorSerializer(author, 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)
#     elif request.method == "DELETE":
#         author.delete()
#         return Response(status=status.HTTP_204_NO_CONTENT)


class AuthorDetailView(APIView):

    def get(self, request, pk):
        try:
            author = Author.objects.get(pk=pk)
        except:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = AuthorSerializer(author)
        return Response(serializer.data)

    def put(self, request, pk):
        try:
            author = Author.objects.get(pk=pk)
        except:
            return Response(status=status.HTTP_404_NOT_FOUND)
        serializer = AuthorSerializer(author, 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)

    def delete(self, request, pk):
        try:
            author = Author.objects.get(pk=pk)
        except:
            return Response(status=status.HTTP_404_NOT_FOUND)
        author.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Как видно различия представлений на базе функций и представлений на базе класса APIView не велики. Вместо явной проверки метода, как в функциях, мы создаем методы внутри классов, названия которых соответствуют необходимому методу. Логику в этих методах при этом оставим точно такую же, как в функциях.

Я специально оставил в закомментированном виде функции, написанные ранее, для наглядности.

api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsListView.as_view()),
    path('authors/<int:pk>', AuthorDetailView.as_view())
]

Возникает вопрос. Зачем использовать класс, если он почти ни чем не отличается от функции? Как упоминалось выше, APIView - базовый класс, а раз есть базовый класс, то есть какие-то дочерние классы. Как в случае обычного django есть базовый класс View и есть наследуемые от него, например, классы ListView и DetailView, так и в случе DRF ситуация аналогичная. Такие классы называются generics классы, и их мы коснемся спустя один раздел. А сейчас познакомимся с программой, которая используется для тестирования API.

Postman

Вы наверное обратили внимание, что мы писали методы POST, PUT, но смотрели только на GET. Открытие адреса в браузере это всегда GET и для тестов других методов DRF предоставляет форму, которую можно увидеть на скриншоте с Author Detail. Эта форма работает и с ее помощью можно проверить запросы отправки данных, но чаще для тестирования подобного api используются специализированные программы, самая известная из них - Postman.

Установите Postman с официального сайта, в установке и регистрации нет ничего особенного.

Сразу откроем Postman и посмотрим, что эта программа из себя представляет.

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

Например, отправим в Postman запрос для отображения всех авторов. Postman показывает нам всю информацию об этом запросе. Во вкладе Body по умолчанию выбран раздел Pretty, тут мы видим список авторов, вид отображения похож на тот, что мы видели в интерфейсе DRF. Если мы хотим посмотреть голый запрос нужно перейти во вкладку Raw. Тут же в Body мы видим формат, по умолчанию JSON, остальные допустимые форматы также видно на скриншоте. Также тут есть информация о статусе, о времени запроса, можно посмотреть заголовки.

Или мы можем воспользоваться разделом Code snippet, который по умолчанию находиться на правой панели. Тут есть много разных вариантов, как этот запрос можно было бы реализовать средствами различных языков и их инструментов. Среди прочих тут есть Python - Requests, выбрав которую мы получаем code для работы с этим запросом, с помощью библиотеки requests.

В общем, Postman мощный инструмент для работы с запросами.

С методом GET думаю все понятно, давайте теперь разбираться с другими методами. Мы воспользуемся нашими классами, но немного позже, сначала предлагаю написать еще один класс, унаследованный от APIView, это нужно для более детального понимания работы этого класса.

api/views.py
from django.forms import model_to_dict
...

...
class AddAuthorView(APIView):

    def post(self, request):
        new_author = Author.objects.create(
            name=request.data['name'],
            country=request.data['country']
        )

        return Response({'author': model_to_dict(new_author)}, status=status.HTTP_201_CREATED)
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    ...
    path('new_author/', AddAuthorView.as_view())
]

Создадим класс AddAuthorView, у которого будет только метод POST и не будет сериализатора, поскольку метод create() мы пропишем прямо в этой модели, а не в сериализаторе. Мы сразу отдадим через Postman данные в формате JSON. Создать этот класс я захотел скорее для лучшего понимая, что содержит в себе data. Как вы видите, в данном примере создаем новую запись в модель методом .create(), где в качестве параметров в нужные поля мы подставляем нужные значения из словаря data. data содержит ключи, соответствующие названиям полей сериализуемой модели, поэтому в прошлых примерах нам достаточно было написать .data, а подставление нужных значений в нужные поля происходит 'за кадром'.

Метод model_to_dict() нужен для вывода записи в качестве ответа на запрос в удобочитаемом формате.

Добавим для этого класса url и воспользуемся этим url внутри программы Postman.

Рассмотрим скриншот. Первым делом обратите внимание в Console, при попытке воспользоваться методом GET для этого адреса мы получаем 405 ответ, говорящий, что для данного url этот метод не поддерживается.

Теперь отправим POST запрос. Для этого среди методов выберем POST и перейдем во вкладку Body, здесь нам нужно переключиться на raw запрос и выбрать формат JSON. Далее можно написать запрос и отправить его.

В ответ мы получаем 201 статус и добавленную запись author, с переданными данными, id при этом сгенерирован автоматически.

shell
>>> from book.models import Author
>>> Author.objects.get(pk=12)
<Author: Фёдор Достоевский>

Для того, чтобы убедиться в добавлении новой записи перейдем в shell и попробуем забрать запись с id 12 из модели Author. В ответ получаем только что добавленную запись.

Для закрепления воспользуемся методом PUT класса AuthorDetailView для изменения записи. Для этого в Postman введем адрес, соответствующий этому классу, выберем 12 запись, которую мы создали выше и метод PUT. Также в Body - raw внесем изменения, например, заменим имя, а страну трогать не будем, значит после отработки метода страна должна остаться неизменной, хотя для частичного изменения, конечно, предпочтительнее использовать метод PATCH. Как видно все отработало без проблем и имя автора 12 записи успешно изменилось.

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

generics

Писать для каждой модели класс представление не обязательно, DRF предоставляет, так называемые, generic классы, унаследованные от APIView, в которых уже написаны соответствующие методы. Их всего 9 и в этом разделе разберемся с каждым.

Самостоятельно ознакомиться с ними можно в соответствующем разделе документации.

Начнем с изменения представления для модели Author.

api/views.py
...
from rest_framework import generics

from book.models import Author
from api.serializers import AuthorSerializer


# class AuthorsListView(APIView):
#     def get(self, request):
#         authors = Author.objects.all()
#         serializers = AuthorSerializer(authors, many=True)
#         return Response(serializers.data, status=status.HTTP_200_OK)
#
#     def post(self, request):
#         serializer = AuthorSerializer(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 AuthorsListView(generics.ListAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsListView.as_view()),
    # path('authors/<int:pk>', AuthorDetailView.as_view()),
]

Первым делом посмотрим на ListAPIView. Все generic классы находятся в rest_framework.generics. ListAPIView используется для метода GET. Любой generic класс имеет два обязательных параметра queryset и serializer_class. Теперь перейдя по адресу данного представления, мы увидим список авторов. ListAPIView реализует только метод GET, а в нашем изначальном классе AuthorsListView было 2 метода - GET и POST.

api/views.py
...
from rest_framework import generics

from book.models import Author
from api.serializers import AuthorSerializer


# class AuthorsListView(APIView):
#     def get(self, request):
#         authors = Author.objects.all()
#         serializers = AuthorSerializer(authors, many=True)
#         return Response(serializers.data, status=status.HTTP_200_OK)
#
#     def post(self, request):
#         serializer = AuthorSerializer(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 AuthorsListView(generics.ListAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer


class AuthorsCreateView(generics.CreateAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsListView.as_view()),
    path('author_create/', AuthorsCreateView.as_view()),
    # path('authors/<int:pk>', AuthorDetailView.as_view()),
]

Для реализации метода POST используется CreateAPIView. Перейдя по адресу, мы увидим.

По данному адресу мы видим форму для отправки POST запроса и 405 статус, связанный с тем, что метод GET для этого View не реализован.

Тем не менее, POST форма прекрасно работает, при этом статусы ответа уже реализованы в generic классах и явно прописывать их не требуется.

Но в нашем изначальном классе было реализовано 2 метода сразу, которые работали по одному адресу, конечно, такого же поведения хочется добиться, используя generic классы. И среди generic классов есть такие, в которых записано сразу несколько методов.

api/views.py
...
from rest_framework import generics

from book.models import Author
from api.serializers import AuthorSerializer


# class AuthorsListView(APIView):
#     def get(self, request):
#         authors = Author.objects.all()
#         serializers = AuthorSerializer(authors, many=True)
#         return Response(serializers.data, status=status.HTTP_200_OK)
#
#     def post(self, request):
#         serializer = AuthorSerializer(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 AuthorsListView(generics.ListCreateAPIView):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsListView.as_view()),
    # path('authors/<int:pk>', AuthorDetailView.as_view()),
]

Для одновременного использования GET и POST есть ListCreateAPIView. По выбранному адресу теперь доступен GET и POST, как и было изначально в классе AuthorsListView. Только вместо 10 строчек теперь используется 2.

Классы ListAPIView и ListCreateAPIView работают для списка записей, остальные классы работают с одной единственной записью.

По аналогии с AuthorsListView изменим AuthorDetailView.

api/views.py
...
from rest_framework import generics

from book.models import Author
from api.serializers import AuthorSerializer

...

# class AuthorDetailView(APIView):
#
#     def get(self, request, pk):
#         try:
#             author = Author.objects.get(pk=pk)
#         except:
#             return Response(status=status.HTTP_404_NOT_FOUND)
#         serializer = AuthorSerializer(author)
#         return Response(serializer.data)
#
#     def put(self, request, pk):
#         try:
#             author = Author.objects.get(pk=pk)
#         except:
#             return Response(status=status.HTTP_404_NOT_FOUND)
#         serializer = AuthorSerializer(author, 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)
#
#     def delete(self, request, pk):
#         try:
#             author = Author.objects.get(pk=pk)
#         except:
#             return Response(status=status.HTTP_404_NOT_FOUND)
#         author.delete()
#         return Response(status=status.HTTP_204_NO_CONTENT)


class AuthorDetailView(generics.RetrieveAPIView):  # GET for single model instance
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer


class AuthorUpdateView(generics.UpdateAPIView):  # PUT and PATCH for single model instance
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer


class AuthorDeleteView(generics.RetrieveDestroyAPIView):  # GET and DELETE for single model instance
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer


class AuthorDetailUpdateView(generics.RetrieveUpdateAPIView):  # GET, PUT and PATCH for single model instance
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer


class AuthorDetailUpdateDeleteView(generics.RetrieveUpdateDestroyAPIView):  # GET, PUT, PATCH and DELETE for single model instance
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer

Сразу на примере этого класса можем рассмотреть все остальные generic классы.

RetrieveAPIView - метод GET для единичной модели. При этом, несмотря на то, что в queryset мы пишем метод .all(), все записи при этом мы не перебираем. В данном случае происходит 'ленивый' запрос, то о чем мы говорили с вам в блоке 'Django. ORM', поэтому не переживайте лишней нагрузки на БД данный класс не создает.

UpdateAPIView - методы PUT и PATCH. Если кто-то еще не понимает в чем отличие этих методов, PUT - подразумевает изменение всех полей записи, PATCH - некоторых полей. При этом, если вы помните, когда в Postman мы проверяли работает ли метод PUT и для проверки изменяли всего одно поле - изменение прошло успешно. Это говорит нам о том, что и PUT подходит для выборочных изменений записи, но при этом PATCH не пробегается по всем полям записи, а задевает только нужное, в то время как PUT, даже несмотря на то, что в нем изменяется какое-то одно конкретное поле все-равно пробегается по всем полям. Не забывайте про этот момент.

RetrieveDestroyAPIView - методы GET и DELETE.

RetrieveUpdateAPIView - методы GET, PUT и PATCH.

RetrieveUpdateDestroyAPIView - метод для полной работы над одной записью. Реализация, так называемой, CRUD (Create, Read, Update, Delete) операции. Соответственно методы GET, PUT, PATCH и DELETE.

Несколько сериализаторов для одной модели

Мы разобрали реализацию методов для модели Author, но в нашем проекте есть еще несколько моделей. Модель Book - центральная модель проекта с большим, на фоне остальных моделей, количеством полей.

Допустим на главную страницу мы хотим забирать только поля 'id' и 'title', но в целом, мы, конечно, хотим сериализовать данные по всем полям модели. Для этого мы можем написать два сериализатора.

api/serializers.py
from rest_framework.serializers import ModelSerializer
from book.models import Author, Book

...

class BookSerializer(ModelSerializer):
    class Meta:
        model = Book
        fields = '__al__'


class ThinBookSerializer(ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title']

Например, сделаем это так. Но как вы понимаете, если мы будем использовать один из generic классов для работы над моделью Book, то и сериализатор мы сможем использовать только один.

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

.../lib/python3.9/dist-packages/rest_framework/generics.py
...
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)
...

Например, два класса - UpdateAPIView и ListCreateAPIView, эти классы как и другие наследуются от GenericAPIView и миксинов отвечающих за разные методы.

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

Давайте так и сделаем.

Посмотрим на ListCreateAPIView, класс имеет методы get и post. Обратите внимание на метод get, он возвращает функцию .list(), но это не тот .list(), который мы знаем из базового синтаксиса python. Для того чтобы посмотреть на него, 'провалимся' в ListModelMixin.

.../lib/python3.9/dist-packages/rest_framework/mixins.py
...
class ListModelMixin:
    """
    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)
...

Реализован он следующим образом, в него попадает queryset и serializer, которые мы передаем сами при обращении к generic классу, который наследуется от этого миксина.

Значит мы можем написать свой класс, унаследоваться от этого миксина и поскольку функция .list() вызывается для метода get, то мы изменим только метода get.

api/views.py
...
from rest_framework import generics
from rest_framework import mixins

from book.models import Author, Book
from api.serializers import AuthorSerializer, BookSerializer, ThinBookSerializer


class BookListView(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView):  # == class BookListView(generic.ListCreateAPIView)
    queryset = Book.objects.all()
    serializer_class = BookSerializer

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


# class BookListView(generics.ListAPIView):
#     queryset = Book.objects.all()
#     serializer_class = ThinBookSerializer
...

Вот, что мы можем сделать. Посмотрим сначала на BookListView, который стоит в комментарии. ListAPIView наследуется от GenericAPIView и ListModelMixin, поддерживает только метод get. Поэтому мы сразу передадим нужный сериализатор, он попадет в метод get и мы будем забирать только 'id' и 'title'. Но если мы хотим реализовать то о чем говорили выше, нам потребуется класс, который поддерживает несколько методов. Как раз класс ListCreateAPIView, который мы рассмотрели выше. Передадим в общий serializer_class сериализатор, в котором обрабатываются все поля. А в serializer_class метода get тот, который обрабатывает 2 поля. Таким образом при выводе записей на экран мы будем видеть только id и title, но если мы захотим добавить новую запись, то мы можем создать ее со всеми поддерживаемыми полями.

ViewSets

Вы наверное заметили, что для описания методов модели Author мы использовали несколько классов. То есть для реализации методов просмотра всего списка и добавления к нему записей использовался класс AuthorsListView. А детальной работы с одной записью - AuthorDetailUpdateDeleteView. При этом наблюдается повторение кода, а именно строчек queryset = и serializer_class =. А если у нас в проекте много моделей, получается для каждой будет лишнее дублирование и захламление когда лишними строчками.

Для избежания такой ситуации в DRF предусмотрен механизм под названием ViewSets.

Интересуют нас классы ReadOnlyModelViewSet и ModelViewSet.

.../lib/python3.9/dist-packages/rest_framework/viewsets.py
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
                           mixins.ListModelMixin,
                           GenericViewSet):
    """
    A viewset that provides default `list()` and `retrieve()` actions.
    """
    pass


class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass

Представляют оно собой следующее. Оба наследуются от GenericViewSet, который в свою очередь от ViewSetMixin и GenericAPIView, в этих двух классах написаны все необходимые методы, такие как as_view(), get_serializer(), get_serializer_class(), get_queryset() и прочее, то есть все необходимые методы для работы с моделями в DRF средствами Django хранят в себе эти классы. Миксины соответственно хранят в себе методы создания, редактирования, просмотра и прочее с чем мы раньше уже сталкивались.

ModelViewSet поддерживает в себе все возможные методы, то есть просмотр всех записей и добавление новой, а также CRUD операции над каждой конкретной записью этой модели.

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

api/views.py
from rest_framework import viewsets

from book.models import Author, Book
from api.serializers import AuthorSerializer, BookSerializer


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer


class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer

Заменим представления для моделей Author и Book на ViewSet, таким образом теперь для каждой модели доступны все методы всего за 3 строчки кода для каждой.

Мы разобрали с вами написание сериализаторов и представлений для этих сериализаторов с самого низкого уровня до ViewSet и ModelSerializer и понимаем, что под этими классами кроется.

Routers

Для полной реализации ViewSet'ов осталось произвести некоторые изменения с маршрутами.

api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsViewSet.as_view()),
    path('authors/<int:pk>', AuthorsViewSet.as_view()),

    path('books/', BookViewSet.as_view()),
    path('books/<int:pk>', BookViewSet.as_view())
]

Во-первых, на лицо дублирование кода, во-вторых, при таком написании сервер просто не запуститься.

Terminal
TypeError: The `actions` argument must be provided when calling `.as_view()` on a ViewSet. For example `.as_view({'get': 'list'})`

Примерно такого содержания, текст ошибки подсказывает нам, что для каждого метода нужно явно прописать его аналог из миксинов (как вы, надеюсь, помните, list - метод из ListModelMixin, соответствующий методу get).

api/urls.py
from django.urls import path
from api.views import *

urlpatterns = [
    path('authors/', AuthorsViewSet.as_view({'get': 'list', 'post': 'create'})),
    path('authors/<int:pk>', AuthorsViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'})),

    path('books/', BookViewSet.as_view({'get': 'list', 'post': 'create'})),
    path('books/<int:pk>', BookViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}))
]

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

Но выглядит это все достаточно нагружено. Разработчики DRF позаботились об этом дублировании и предоставили для нас механизм - Routers.

api/urls.py
# from django.urls import path
from api.views import *
from rest_framework import routers

router = routers.SimpleRouter()
router.register('authors', AuthorsViewSet)
router.register('books', BookViewSet)
urlpatterns = router.urls


# urlpatterns = [
#     path('authors/', AuthorsViewSet.as_view({'get': 'list', 'post': 'create'})),
#     path('authors/<int:pk>', AuthorsViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update',
#                                                      'delete': 'destroy'})),
#
#     path('books/', BookViewSet.as_view({'get': 'list', 'post': 'create'})),
#     path('books/<int:pk>', BookViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update',
#                                                 'delete': 'destroy'}))
# ]

Реализуется это так. Есть два класса SimpleRouter() и DefaultRouter().

В данном случае мы воспользовались SimpleRouter() и получили все адреса и методы, которые теперь находятся в комментарии. Коллекция urls у переменной router сформировалась автоматически.

Для понимания в чем разница между SimpleRouter() и DefaultRouter() перейдем в браузер и посмотрим на какой-нибудь адрес.

У нас есть стандартный маршрут api/v1/, а после него уже пишутся индивидуальные маршруты для работы с каждой моделью.

И если сейчас перейти по адресу http://127.0.0.1:8000/api/v1/ мы получим ошибку, говорящую, что такой адрес не существует. А если SimpleRouter() заменить на DefaultRouter(), то по данному адресу мы увидим следующее.

По главному адресу api/v1/ теперь есть две ссылки на индивидуальное api для каждой модели. Это вся разница между SimpleRouter() и DefaultRouter().

Permissions

На данный момент любому пользователю сайта не зависимо ни от чего по api доступны любые действия над данными из БД. Конечно, такая избыточная свобода пользователей ни одному сайту не нужна. DRF предоставляет свой механизм ограничения доступа - Permissions.

Базово DRF предоставляет 4 возможны состояния.

AllowAny - доступ всем для всего.

IsAuthenticated - доступ для всего только у авторизованных пользователей.

IsAdminUser - доступ для всего только у пользователей с правами администратора. (или user.is_stuff is True)

IsAuthenticatedOrReadOnly - доступ для просмотра у всех пользователей, доступ для редактирования только у авторизованных.

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

api/views.py
from rest_framework import viewsets
from rest_framework import permissions

from book.models import Author, Book
from api.serializers import AuthorSerializer, BookSerializer


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly, )


class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    permission_classes = (permissions.IsAdminUser, )

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

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

Но, как упоминалось выше, встроенных permission классов недостаточно. 'Провалимся' в любой их этих классов и увидим, что все они наследуются от класса BasePermission. При написании своего класса для прав будем наследоваться от BasePermission.

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

api/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Object-level permission to only allow owners of an object to edit it.
    Assumes the model instance has an `owner` attribute.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Instance must have an attribute named `owner`.
        return obj.owner == request.user

Для написания своих прав создадим отдельный файл - permissions.py в нашем приложении api.

И на самом деле, тот кейс, который я описал выше приводится в официальной документации в качестве примера написания собственных прав. Поскольку реализация права с возможностями редактирования записи только для ее автора это очень частый кейс, используемый, наверное, в любом проекте.

Но давайте все-равно его рассмотрим.

BasePermission имеет два метода:

class BasePermission(metaclass=BasePermissionMetaclass):
    """
    A base class from which all permission classes should inherit.
    """

    def has_permission(self, request, view):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

    def has_object_permission(self, request, view, obj):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

has_permission() - метод, в котором контролируются права на просмотр всех записей, одной записи и добавления новой записи.

has_object_permission() - метод, в котором контролируются права на изменение и удаление конкретной записи.

Вернемся к IsOwnerOrReadOnly, переопределен только метод has_object_permission. Что же в нем написано.

if request.method in permissions.SAFE_METHODS:
    return True

True - открытые права. False - закрытые права. В данном проверке мы смотрим на метод. Если метод находится в коллекции SAFE_METHODS возвращаем True. Коллекция SAFE_METHODS содержит 3 метода ('GET', 'HEAD', 'OPTIONS'), то есть в этой строчке мы пишем, что пользователю доступен просмотр в любом случае.

return obj.owner == request.user

В этой строчке происходит проверка, является ли пользователь создателем данной записи. Конечно, такая проверка доступна только для моделей, которая связана с моделью User. obj - ссылка на рассматриваемую в данный момент запись. owner - название поля, по которому в модели происходит связь с User. У нас в модели Book это поле называется user, значит для работы этого класса нужно заменить owner на user.

api/views.py
...
from api.permissions import IsOwnerOrReadOnly


class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly, )
...

Воспользовались мы только has_object_permission, тем не менее просматривать все записи мы тоже можем, поскольку мы унаследовались от BasePermission и метод has_permission в нем уже определен.

if request.method in permissions.SAFE_METHODS:
    # Check permissions for read-only request
else:
    # Check permissions for write request

А еще вот такая полезная подсказка по доступу к просмотру и редактированию есть в документации.

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

api/permissions.py
...

class IsAdminOrReadOnly(permissions.BasePermission):

    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)

    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return True

        return bool(request.user and request.user.is_staff)

И напишем какие-нибудь свои права для модели Author. Например, пусть авторов могут просматривать только авторизованные пользователи, а изменять только пользователи с правами администратора.
В has_permission проверка, что запрос происходит от авторизованного пользователя.
В has_object_permission, что в случае методов редактирования у пользователя есть права администратора.

Вот так достаточно просто можно писать свои особые права.

ViewSets. Углубление

Мы рассмотрели с вами ModelViewSet, но использовали только 3 параметра queryset, serializer_class и permission_classes, в самом простом варианте этого вполне достаточно, но иногда может понадобиться более гибкая настройка, давайте немного модернизируем наши ViewSet'ы.

api/views.py
from rest_framework import viewsets
from rest_framework import permissions

from book.models import Author, Book
from api.serializers import AuthorSerializer, BookSerializer, ThinBookSerializer
from api.permissions import IsOwnerOrReadOnly, IsAdminOrReadOnly


class BookViewSet(viewsets.ModelViewSet):
    # queryset = Book.objects.all()
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly,)

    def get_serializer_class(self):
        if self.action == 'list':
            return ThinBookSerializer
        return BookSerializer

    def get_queryset(self):
        pk = self.kwargs.get('pk')

        if not pk:
            return Book.objects.all()[:1]

        return Book.objects.filter(pk=pk)
...

Ранее мы определили сериализатор для вывода только id и title в случае get запроса. Но в итоге при создании ViewSet'а этот момент был опущен. Для реализации выбора сериализатора переопределим метод get_serializer_class(). Внутри этого метода будем проверять текущий тип запроса через self.action (список всех action мы писали, когда говорили о Routers) и в случае метода list будем использовать сериализатор обрабатывающий 2 поля, в противном случае сериализатор, обрабатывающий все поля.

Для более гибкого queryset мы можем переопределить метод get_queryset(). В данном примере будем забирать pk из коллекции kwargs и проверять есть ли данная запись в нашей БД. Если есть - возвращаем ее с помощью метода .filter(), а не get() потому что мы должны получить queryset. Если нет (в данному случе 'нет' означет, что 'pk' в коллекции kwargs не существует) - возвращаем первую запись, с помощью параметра LIMIT, метод .first() также не подойдет, поскольку он возвращает не queryset. Подробнее об ORM в блоке 'Django. ORM'.

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

Terminal
AssertionError: `basename` argument not specified, and could not automatically determine the name from the viewset, as it does not have a `.queryset` attribute.

Содержание будет примерно следующее. Дело в том, что когда мы пишем router для конкретного ViewSet'а для него автоматически формируется параметр basename, но происходит это только при наличии переменной queryset = во ViewSet'е. Формируется она из названия модели, переданной в параметр queryset и соответствует имени этой модели. Так в случае queryset = Book.objects.all() переменная basename была равна book. Теперь queryset у нас находится в комментарии, метод get_queryset() переменную queryset не формирует, поэтому basename нужно прописать вручную.

api/urls.py
from api.views import *
from rest_framework import routers

router = routers.DefaultRouter()
router.register('authors', AuthorsViewSet)
router.register('books', BookViewSet, basename='book')
urlpatterns = router.urls

Делается в файле urls.py. Добавим basename='book' (название может быть произвольным) и запустим сервер. Теперь запуск должен произойти без ошибок.

Если мы переходим по адресу api/v1/books/, без выбора конкретной записи, срабатывает проверка внутри get_queryset() и возвращается только одна первая запись. А в методе get_serializer_class() происходит проверка метода, метод соответствует get, поэтому мы получаем информацию только о полях 'id' и 'title'.

А если перейти по адресу конкретной записи, то в методе get_queryset() срабатывает другая проверка и мы возвращаем запись соответствующего, а в методе get_serializer_class() метод уже соответствует не get, а retrieve, поэтому мы видим информацию о всех полях.

Займемся теперь AuthorsViewSet.

api/views.py
...
class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer

    def get_permissions(self):
        if self.action in ('list', 'retrieve'):
            self.permission_classes = [permissions.IsAuthenticated, ]
        else:
            self.permissions_classes = [permissions.IsAdminUser, ]

        return super(self.__class__, self).get_permissions()

Давайте сделаем более тонкую настройку прав, но не с помощью нового класса permissions.py, а с помощью метода get_permissions(). Внутри него делаем проверку соответствия действию и в зависимости от действия используем разные права.

api/views.py
class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    http_method_names = ['get', 'delete']

    def get_permissions(self):
        if self.action in ('list', 'retrieve'):
            self.permission_classes = [permissions.IsAuthenticated, ]
        else:
            self.permissions_classes = [permissions.IsAdminUser, ]

        return super(self.__class__, self).get_permissions()

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

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

Так можно переопределять некоторые методы для более гибкой настройки viewset'а. Некоторые возможности мы еще рассмотрим далее.

Serializers. Углубление

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

Рассмотрим модель Book, поскольку у нее есть и ForeignKey и ManyToManyField, это нам сейчас как раз интересно.

Перейдем в какую-нибудь книгу, под пользователем, который к ней прикреплен.
Первая проблема. В полях категория, авторы и пользователь мы видим id, хотелось бы видеть, например, имя.
Вторая проблема. Возможность изменения пользователя, и пусть при PUT это может быть не так критично, хотя, конечно, тоже не желательно, но такая же возможность появляется и при POST, а это нам уже совсем не нужно. И вообще, поле user должно заполняться автоматически при создании записи, но об этом чуть позже. Сначала разберемся с первыми двумя вопросами.

api/serializers.py
from rest_framework.serializers import ModelSerializer, SlugRelatedField
from book.models import Author, Book, Category

...

class ThinBookSerializer(ModelSerializer):
    class Meta:
        model = Book
        fields = ['id', 'title']


class BookSerializer(ModelSerializer):
    category = SlugRelatedField(slug_field='name', queryset=Category.objects.all())
    user = SlugRelatedField(slug_field='username', read_only=True)
    authors = SlugRelatedField(
        slug_field='name', many=True, queryset=Author.objects.all()
    )

    class Meta:
        model = Book
        fields = ['id', 'title', 'slug', 'description', 'year', 'category', 'authors', 'user']

Поля, у которых мы хотим видеть вместо id значение нужно пропустить через SlugRelatedField. В параметре slug_field указывается название поля, значение которого мы хотим видеть вместо id. Так совпало, что и в модели Category, и в модели Author это поле мы назвали name, в модели User стандартное имя для этого поля - username. SlugRelatedField должен либо возвращать queryset, либо иметь параметр read_only в значении True.
Параметр read_only=True используется для тех полей, информацию о которых мы ббудем только выводить дял просмотра, но не позволим изменять, как раз то условие, которое нам нужно для User.
queryset соответственно мы возвращаем, когда данные предполагают изменение.
И также не стоить забывать, что в случае ManyToManyField необходимо использовать параметр many=True, в противном случае, вместо значений мы получим None.

Вернемся на ту же запись. Вместо id теперь мы видим имена, а поле user не доступно для изменения. То чего мы и добивались.

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

api/serializers.py
...
class BookSerializer(ModelSerializer):
    category = SlugRelatedField(slug_field='name', queryset=Category.objects.all())
    # user = SlugRelatedField(slug_field='username', read_only=True)

    user = SerializerMethodField(read_only=True)

    def get_user(self, obj):
        return obj.user.username

    authors = SlugRelatedField(
        slug_field='name', many=True, queryset=Author.objects.all()
    )

    class Meta:
        model = Book
        fields = ['id', 'title', 'slug', 'description', 'year', 'category', 'authors', 'user']
api/views.py
...
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly,)

    def perform_create(self, serializer):
        serializer.save(user=self.request.user)

    def get_serializer_class(self):
        if self.action == 'list':
            return ThinBookSerializer
        return BookSerializer

    def get_queryset(self):
        pk = self.kwargs.get('pk')

        if not pk:
            return Book.objects.all()[:1]

        return Book.objects.filter(pk=pk)

Для этого воспользуемся методом perform_create(), внутри BookViewSet. Этот метод напоминает параметр commit=False, когда речь идет об обычных django views. Перед полным сохранением сериализованных данных мы вручную контролируем одно из полей, в данном случае помещаем в поле user имя пользователя из request. Так в две строки можно реализовать автоматическое добавление автора.

В serializers.py изменения совсем необязательны, ту запись, которую я привел вместо использования SlugRelatedField это просто альтернативный вариант. Класс SerializerMethodField позволяет создавать методы полей вида, например, get_(название поля), внутри этого метода мы получаем имя пользователя и оно выводиться, когда мы заходим на страницу конкретной записи. То же самое, что мы реализовали выше. 

Думаю теперь мы более глубоко понимаем как можно работать с сериализаторами.

@action

Существует еще одна возможность связанные с drf view, о которой необходимо упомянуть. Мы знакомы с тем, что такое action в drf. action - метод, который ассоциирован с HTTP запросом.
list - get. Просмотр списка записей.
retrieve - get. Просмотр одной записи (детализация).
create - post. Добавление новой записи.
update - put. Полное изменение данных конкретной записи.
partial_update - patch. Частичное изменение данных конкретной записи.
destroy - delete. Удаление записи.

Стандартно предоставляются следующий набор action'ов. Мы с вами видели как устроен action list. Все эти методы возвращают Response, то есть данные приведенные к json формату. И набор этих самых action'ов как вы видите ограничен. Но иногда может появиться необходимость в написании своего action.

DRF предоставляет нам декоратор @action, с помощью которого мы можем сделать свою ассоциацию с каким-нибудь из методов или набором методов.

api/views.py
...
from rest_framework import decorators
from rest_framework.response import Response

from book.models import Author, Book, Category
...


class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly,)

    @decorators.action(methods=['get'], detail=False)
    def authors(self, request):
        authors = Author.objects.all()
        return Response({'authors': [author.name for author in authors]})

    @decorators.action(methods=['get'], detail=False)
    def categories(self, request):
        categories = Category.objects.all()
        return Response({'categories': [category.name for category in categories]})
...

Декоратор @action находится в rest_framework.decorators. Принимает 2 параметра, в methods списком передаются методы, которые будет поддерживать этот декоратор; detail, в значении False - для работы с набором записей, в значении True - для работы с одной записью.

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

Создадим два одинаковых action, первый для вывода авторов, второй для вывода категорий. Вывод авторов у нас есть отдельно, но если бы нам было достаточно только просмотра данных из прочих модели, то нам было бы достаточно использовать action. Забираем queryset записей и перебирая их в Response создаем список и помещаем его в словарь. В данном примере будем забирать имена авторов и названия категорий.

По адресу http://127.0.0.1:8000/api /v1/books/authors/ мы теперь видим список авторов. Ключ "authors", который мы писали в Response(), значение ключа список имен авторов. Адрес http://127.0.0.1:8000/api/v1/books/ принадлежит BookViewSet, дополнение authors/ к адресу сформировано из-за названия метода.

Соответственно по адресу http://127.0.0.1:8000/api /v1/books/categories/ мы увидим список категорий.

api/views.py
...
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly,)

    ...

    @decorators.action(methods=['get'], detail=False)
    def authors_and_categories(self, request):
        authors = Author.objects.all()
        categories = Category.objects.all()
        return Response({'authors': [author.name for author in authors],
                         'categories': [category.name for category in categories]})
...

Для примера давайте напишем еще action для вывода и авторов и категорий. Для этого по сути объединим два метода в один, поместив оба queryset в один словарь.

По адресу http://127.0.0.1:8000/api /v1/books/authors_and_categories/ получаем список авторов и категорий. С выводом списка записей думаю разобрались.

С выводом одной записи есть некоторая особенность.

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

Добавим pk в параметр метода и вместо queryset заберем одну запись, например, методом get().

api/views.py
...
class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = (IsOwnerOrReadOnly,)

    ...

    @decorators.action(methods=['get', 'post'], detail=True)
    def author(self, request, pk):
        author = Author.objects.get(pk=pk)
        return Response({'author': author.name})
...

Все верно, в метод передадим pk, возьмем запись соответсвующую этому pk, вместо списка записей в Response() отдадим содержимое поля name и переведем параметр detail в значение False.

Но давайте теперь попробуем перейти по адресу какой-нибудь записи.

Ошибка. Из которой мы видим, что по адресу мы ожидаем pk записи, а получаем 'author'. Всегда когда мы обращаемся к конкретной записи мы передаем ее pk и данный случай не исключение.

Но если мы попробуем добавить pk, мы получим 404 ответ. Дело в том, что у action'ов для детализации есть одна особенность, pk для обращения к ним пишется перед именем адреса.

Поместив 2 перед author, мы видим детализацию конкретной записи. Мы так подробно остановились на этом, что думаю, если вам придется action для вывода конкретной записи вы сразу вспомните об этой особенности.

И обращаю ваше внимание, action author написан для модели Book, поэтому метод post предлагает нам добавить запись именно к этой модели, а не к модели Author.

Пагинация

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

Сначала посмотрим как реализовывается пагинация для всего проекта.

pylibrary/settings.py
...
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}
...

Глобальные настройки DRF для всего проекта записываются в файле settings.py в переменной REST_FRAMEWORK.

В самом простом варианте нам достаточно написать одну переменную - DEFAULT_PAGINATION_CLASS. Различают несколько типов встроенных классов пагинации, начнем с LimitOffsetPagination. Из названия выделяются два слова Limit и Offset, давайте сразу на примере посмотрим как их можно использовать, и что они означают.

Для этого перейдем например на страницу списка авторов.

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

Параметр limit используется для контролирования количества записей на одной странице. Добавим к запросу ?limit=1. Теперь на странице мы видим одну запись, а в правом верхнем углу появился стандартный drf интерфейс пагинации.

Параметр offset означает сколько страниц пропустить перед выводом. Так, добавив параметр offset=2 (перечисляются параметры в запросе через знак амперсанда), мы появляемся сразу на третьей странице.

Каждый раз писать параметры пагинации в запросе может быть не очень удобно. Пагинация поддерживает добавления параметра default_limit, в котором как раз запишем количество страниц для параметра limit по умолчанию. Запишем его в settings.py

pylibrary/settings.py
...
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 1
}
...

Задается этот параметр в переменной PAGE_SIZE словаря REST_FRAMEWORK. Теперь при переходе на любые, созданные нами, адреса мы будем видеть одну запись. При этом, параметр можно всегда изменить в адресной строке вручную, то есть ничего нам не помешает поменять ?limit=1 на, например, ?limit=3 и в рамках данного получить вывод 3 записей, но стоит нам перезайти на этот адрес и limit снова будет равен дефолтному значению.

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

pylibrary/settings.py
...
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 1
}
...

PageNumberPagination вместо параметров limit и offset использует параметр page.

PAGE_SIZE также содержит количество выводимых на странице записей, параметр запроса page= указывает на номер страницы.

Последний класс пагинации CursorPagination. Но если написать его в REST_FRAMEWORK, как мы делали в прошлые разы, то мы получим ошибку. Ошибка возникнет только в том случае, если у вас в модели не определено поле created. Дело в том, что CursorPagination по умолчанию имеет параметр ordering, и когда мы объявляем этот класс в settings.py, то параметр ordering принимает значение поля created (или если говорить точнее -created). Конечно, не каждая модель содержит поле created и совершенно не обязательно писать его для каждой модели. Этот параметр можно переопределить, но сделать это в словаре REST_FRAMEWORK не выйдет.

И так мы плавно подошли к работе с пагинацией внутри view.

api/views.py
...
from rest_framework.pagination import CursorPagination
...

class CursorSetPagination(CursorPagination):
    page_size = 1
    ordering = 'id'


class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    http_method_names = ['get', 'delete']
    pagination_class = CursorSetPagination

    def get_permissions(self):
        if self.action in ('list', 'retrieve'):
            self.permission_classes = [permissions.IsAuthenticated, ]
        else:
            self.permissions_classes = [permissions.IsAdminUser, ]

        return super(self.__class__, self).get_permissions()

Пользовательская настройка классов пагинации пишется вместе с view. Находятся все классы в rest_framework.pagination. Импортируем CursorPagination и создадим свой класс на его основе. CursorSetPagination будет содержать всего 2 параметра page_size и ordering. page_size количество элементов на странице. ordering параметр, из-за которого мы и получали ошибку. Заменим значение ordering на существующее поле.

Внутри AuthorsViewSet запишем параметр pagination_class, в который и передадим наш CursorSetPagination. Как вы понимаете в этот параметр мы можем передать и любой из классов по умолчанию.

Пагинация на основе CursorSetPagination предоставляет интерфейс пагинации с кнопками Previous и Next.

И применена такая пагинация будет только к AuthorsViewSet. Для остальных моделей мы можем написать логику пагинации как в REST_FRAMEWORK, так и индивидуально для каждой, либо вообще не писать.

Дросселирование

Дросселирование (Throttling) - ограничение количества допустимых запросов к нашему api. Обычно любое api имеет ограничение измеряемое как количество запросов/секунда. Делается это чтоб не перегружать api. Каждый запрос к api - дополнительная нагрузка на сервер. Представьте сколько пользователей у какого-нибудь популярного сервиса, который предоставляет открытую api документацию. Конечно, не все пользователи сервисов пользуются их api, это малая часть, но при популярности ресурса, сами понимаете, в какие теоретические нагрузки может вылиться неограниченность запросов.

Так, например, github имеет ограничение 30 запросов в секунду к своему api.

В DRF дросселирование настраивается достаточно просто.

pylibrary/settings.py
...
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 1,

    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '40/second'
    }

}
...

Добавим их в словарь REST_FRAMEWORK следующим образом.
Список DEFAULT_THROTTLE_CLASSES содержит два класса дросселирования - AnonRateThrottle для неавторизованных пользователей, UserRateThrottle - для авторизованных.
Словарь DEFAULT_THROTTLE_RATES - количество запросов для каждого типа пользователей. Может быть задано в секундах, минутах, часах и днях.

Так пусть для авторизованных пользователей будет лимит в 40 запросов/секунда, а для неавторизованных 100/день.

Дросселирование можно задавать для каждого view индивидуально.

api/views.py
...
from rest_framework.throttling import UserRateThrottle, AnonRateThrottle
...

class CursorSetPagination(CursorPagination):
    page_size = 1
    ordering = 'id'


class AuthorsViewSet(viewsets.ModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer
    http_method_names = ['get', 'delete']
    pagination_class = CursorSetPagination
    throttle_classes = ['UserRateThrottle', ] # == @throttle_classes([UserRateThrottle]) для def

    def get_permissions(self):
        if self.action in ('list', 'retrieve'):
            self.permission_classes = [permissions.IsAuthenticated, ]
        else:
            self.permissions_classes = [permissions.IsAdminUser, ]

        return super(self.__class__, self).get_permissions()

Записываются они в переменной throttle_classes. Если вы используете функции вместо классов, то используйте декоратор @throttle_classes().

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

Что дальше?

К этому моменту мы познакомились почти со всеми базовыми возможностями Django REST framework. Почему с базовыми? Потому что DRF очень востребованный и популярный инструмент и, конечно, для него есть немалое количество дополнительных библиотек. Следующий блок по DRF будет посвящен в большей степени работе с этими библиотеками. В первую очередь нам предстоит разговор о механизмах аутентификации, хоть некоторые из них относятся как раз к базовым возможностям DRF, но думаю логичнее будет рассмотреть их все в одном месте. И, помимо аутентификации, есть еще несколько возможностей, которые хотелось бы обсудить.

Для отправки комментария необходимо авторизоваться



Комментарии

Здесь пока ничего нет...