Блок 1 — Python Basic (1–6)
- Лекция 1. Введение. Типизации. Переменные. Строки и числа. Булева алгебра. Ветвление
- Лекция 2. Обработка исключений. Списки, строки детальнее, срезы, циклы.
- Лекция 3. None. Range, list comprehension, sum, max, min, len, sorted, all, any. Работа с файлами
- Лекция 4. Хеш-таблицы. Set, frozenset. Dict. Tuple. Немного об импортах. Namedtuple, OrderedDict
- Лекция 5. Функции, типизация, lambda. Map, zip, filter.
- Лекция 6. Алгоритмы и структуры данных
Блок 2 — Git (7–8)
Блок 3 — Python Advanced (9–14)
- Лекция 9. Введение в ООП. Основные парадигмы ООП. Классы и объекты. Множественное наследование.
- Лекция 10. Magic methods. Итераторы и генераторы.
- Лекция 11. Imports. Standard library. PEP 8
- Лекция 12. Декораторы. Декораторы с параметрами. Декораторы классов (staticmethod, classmethod, property)
- Лекция 13. Тестирование
- Лекция 14. Проектирование. Паттерны. SOLID.
Блок 4 — SQL (15–17)
Блок 5 — Django (19–26)
- Лекция 19. Знакомство с Django
- Лекция 20. Templates. Static
- Лекция 21. Модели. Связи. Meta. Abstract, proxy
- Лекция 22. Django ORM
- Лекция 23. Forms, ModelForms. User, Authentication
- Лекция 24. ClassBaseView
- Лекция 25. NoSQL. Куки, сессии, кеш
- Лекция 26. Логирование. Middleware. Signals. Messages. Manage commands
Блок 6 — Django Rest Framework (27–30)
- Лекция 27. Что такое API. REST и RESTful. Django REST Framework
- ▶ Лекция 28. @api_view, APIView, ViewSets, Pagination, Routers
- Лекция 29. REST-аутентификация. Авторизация. Permissions. Фильтрация.
- Лекция 30. Тестирование. Django, REST API.
Блок 7 — Python async (31–33)
Блок 8 — Deployment (34–35)
Все мы помним, что веб в первую очередь — это Request-Response система.
Что нового в request.
Документация: https://www.django-rest-framework.org/api-guide/requests/
Два новых параметра .data и .query_params
.data — данные, если запрос POST, PUT или PATCH, аналог request.POST или request.FILES
.query_params — данные, если запрос GET, аналог request.GET
Также доступны request.user и request.auth. Детали аутентификации/авторизации рассмотрим на следующей лекции.
Практические нюансы:
request.dataобъединяет данные из JSON/формы/файлов в зависимости от подключённых парсеров (JSONParser, FormParser, MultiPartParser)request.query_params— это QueryDict (поддерживает несколько значений для одного ключа)- Полезные атрибуты:
request.content_type,request.accepted_renderer,request.accepted_media_type - Расширяйте DEFAULT_PARSER_CLASSES, если нужны формы/файлы
Пример настройки парсеров (частично):
REST_FRAMEWORK = {
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.MultiPartParser",
"rest_framework.parsers.FormParser",
],
}Документация: https://www.django-rest-framework.org/api-guide/responses/
В отличие от классической Django, ответом в REST системе будет обычный HTTP-ответ, содержащий набор данных, чаще всего JSON (но бывает и нет).
Классическая Django тоже может возвращать HTTP-ответ и быть обработчиком REST архитектуры, но существующий пакет сильно упрощает эти процессы.
Для обработки такого ответа есть специальный объект:
Response(data, status=None, template_name=None, headers=None, content_type=None)
Пример ответа при создании ресурса (201 + Location):
from rest_framework import status
# после serializer.save() -> obj
return Response(serializer.data, status=status.HTTP_201_CREATED, headers={"Location": obj.get_absolute_url()})где data — данные,
status — код ответа (200, 404, 503),
template_name — возможность указать темплейт, если необходимо вернуть страницу, а не просто набор данных,
headers и content_type — заголовки и тип содержимого запроса.
request.data— тело запроса (JSON/форма/файлы)request.query_params— параметры строки запроса (?page=1&search=...)request.FILES— загружаемые файлы- Возвращайте Response(data, status=...) и используйте константы из rest_framework.status, например status.HTTP_201_CREATED
- Если отключить BrowsableAPIRenderer, «Browsable API» исчезнет и ответы будут только в JSON
Если нужно указать явно формат для получения или отправки, то можно указать его в settings.py:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
]
}Документация: https://www.django-rest-framework.org/api-guide/views/#api_view
Для описания endpoint функционально нужно указать декоратор api_view и методы, которые он может принимать.
Возвращает всё также объект ответа. Для использования возьмём модель Article и сериалайзер ArticleSerializer из нашего блога:
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from blog.models import Article
from blog.serializers import ArticleSerializer
@api_view(['GET', 'POST'])
def article_list(request):
"""
Список всех статей или создание новой статьи.
"""
if request.method == 'GET':
# Показываем только опубликованные статьи
articles = Article.objects.filter(status='published')
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = ArticleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)Обратите внимание, в пакете REST фреймворка сразу есть заготовленные объекты статуса для ответа.
Если попытаться получить доступ методом, который не разрешен, запрос будет отклонён с ответом 405 Method not allowed
Для передачи параметров используются аргументы функции. (Очень похоже на обычную Django вью)
from django.shortcuts import get_object_or_404
@api_view(['GET', 'PUT', 'DELETE'])
def article_detail(request, pk):
"""
Получение, обновление или удаление статьи.
"""
article = get_object_or_404(Article, pk=pk)
if request.method == 'GET':
serializer = ArticleSerializer(article)
return Response(serializer.data)
elif request.method == 'PUT':
serializer = ArticleSerializer(article, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
elif request.method == 'DELETE':
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)URL для таких методов описываются точно так же, как и для стандартной Django вью:
from blog.views import article_list, article_detail
urlpatterns = [
path('articles/', article_list),
path('articles/<int:pk>/', article_detail),
]Ответ на GET-запрос в этом случае будет выглядеть так:
[
{
"id": 1,
"title": "Введение в Django REST Framework",
"slug": "intro-to-drf",
"status": "published",
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"title": "Продвинутые сериализаторы",
"slug": "advanced-serializers",
"status": "published",
"created_at": "2024-01-16T14:20:00Z"
}
]Поля будут зависеть от модели и сериалайзера соответственно.
Ответ на POST запрос (создание объекта):
{
"id": 3,
"title": "Новая статья",
"slug": "new-article",
"status": "draft",
"created_at": "2024-01-17T09:00:00Z"
}Справочник по CBV/DRF: https://www.cdrf.co/
- Быстрое функциональное представление: @api_view — простые эндпоинты без классов, минимум кода
- Точная кастомизация: APIView — полный контроль над методами get/post/put/delete
- CRUD из коробки по одному ресурсу: GenericAPIView + миксины (ListCreateAPIView, RetrieveUpdateDestroyAPIView)
- Полный набор CRUD и экшены + автоматический роутинг: ModelViewSet/ReadOnlyModelViewSet + Router (чаще всего на практике)
Документация: https://www.django-rest-framework.org/api-guide/views/#class-based-views
На уровне класса можно настраивать: parser_classes, renderer_classes, throttle_classes. Также мы можем описать это же через Class-Based View, для этого нам нужно наследоваться от APIView:
from blog.models import Article
from blog.serializers import ArticleSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.shortcuts import get_object_or_404
class ArticleListAPIView(APIView):
"""
Список всех статей или создание новой.
"""
def get(self, request, format=None):
articles = Article.objects.filter(status='published')
serializer = ArticleSerializer(articles, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = ArticleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)Вынесем получение объекта в отдельный метод:
from django.shortcuts import get_object_or_404
class ArticleDetailAPIView(APIView):
"""
Получение, обновление или удаление статьи.
"""
def get_object(self, pk):
return get_object_or_404(Article, pk=pk)
def get(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(article)
return Response(serializer.data)
def put(self, request, pk, format=None):
article = self.get_object(pk)
serializer = ArticleSerializer(article, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
def delete(self, request, pk, format=None):
article = self.get_object(pk)
article.delete()
return Response(status=status.HTTP_204_NO_CONTENT)URL описываются так же, как и для Django Class-Based View:
urlpatterns = [
path('articles/', ArticleListAPIView.as_view()),
path('articles/<int:pk>/', ArticleDetailAPIView.as_view()),
]По аналогии с классической 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(), почему?
Потому что их вызов перенесен в дополнительные классы.
Вот так выглядят классы, которые уже можно использовать. Как это работает? Эти классы наследуют логику работы с данными
из необходимого миксина, общие методы, которые актуальны для любого 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)Допустим, мы хотим описать класс для получения списка комментариев к конкретной статье и создания новых комментариев:
from rest_framework.generics import ListCreateAPIView
from blog.models import Comment
from blog.serializers import CommentSerializer
class ArticleCommentListView(ListCreateAPIView):
"""
Список комментариев к статье и создание нового комментария.
"""
serializer_class = CommentSerializer
def get_queryset(self):
# Фильтруем комментарии по статье из URL
article_pk = self.kwargs['article_pk']
return Comment.objects.filter(article_id=article_pk)
def perform_create(self, serializer):
# Автоматически привязываем комментарий к статье и автору
article_pk = self.kwargs['article_pk']
serializer.save(
article_id=article_pk,
author=self.request.user
)URL для такого представления:
urlpatterns = [
path('articles/<int:article_pk>/comments/', ArticleCommentListView.as_view()),
]Всё, этого достаточно для полноценного CRUD комментариев к статье!
Документация: https://www.django-rest-framework.org/api-guide/viewsets/
Классы, которые отвечают за поведение нескольких запросов, и отличаются друг от друга только методом, называются
ViewSet.
Они на самом деле описывают методы, для получения списка действий (list, retrieve, и т. д.), и преобразования их в URL (об этом дальше).
Например:
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 запросе.
Для описания URL можно использовать разные варианты:
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})Хоть так никто и не делает, об этом дальше.
Документация: https://www.django-rest-framework.org/api-guide/viewsets/#modelviewset
Объединяем всё, что мы уже знаем.
И получаем класс ModelViewSet, он наследуется от GenericViewSet (ViewSet + GenericAPIView) и всех 5 миксинов, а
значит там описаны методы list(), retrieve(), create(), update(), destroy(), perform_create(),
perform_update() и т. д.
А значит мы можем описать сущность, которая принимает модель и сериалайзер, и уже может принимать любые типы запросов и выполнять любые CRUD действия. Мы можем их переопределить, или дописать еще экшенов, всё что нам может быть необходимо уже есть.
from rest_framework import viewsets
from blog.models import Article
from blog.serializers import ArticleSerializer, ArticleListSerializer
class ArticleViewSet(viewsets.ModelViewSet):
"""
ViewSet для полного CRUD статей блога.
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
lookup_field = 'pk' # можно использовать 'slug' для красивых URLЭтот простой класс уже предоставляет все CRUD операции:
GET /articles/— список статейPOST /articles/— создание статьиGET /articles/{pk}/— детали статьиPUT /articles/{pk}/— полное обновлениеPATCH /articles/{pk}/— частичное обновлениеDELETE /articles/{pk}/— удаление
Разные сериалайзеры для разных действий:
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
def get_serializer_class(self):
if self.action == 'list':
return ArticleListSerializer # Краткая информация
return ArticleSerializer # Полная информацияДля ресурсов, которые можно только читать (например, темы блога):
from blog.models import Topic
from blog.serializers import TopicSerializer
class TopicViewSet(viewsets.ReadOnlyModelViewSet):
"""
ViewSet только для чтения тем блога.
Доступны только list и retrieve.
"""
queryset = Topic.objects.all()
serializer_class = TopicSerializerМожно собрать ViewSet для любых действий, комбинируя миксины:
from rest_framework import mixins, viewsets
class CreateListRetrieveViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
"""
ViewSet с операциями create, list и retrieve.
Без update и delete.
"""
pass
# Использование
class CommentViewSet(CreateListRetrieveViewSet):
"""
Комментарии можно создавать и читать, но не редактировать/удалять.
"""
queryset = Comment.objects.all()
serializer_class = CommentSerializerЧаще всего используются обычные ModelViewSet.
Документация: https://www.django-rest-framework.org/api-guide/pagination/
Как мы помним, для действия list используется пагинация. Как это работает?
Если у нас нет необходимости настраивать все вьюсеты отдельно, то мы можем указать такую настройку в settings.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100
}Пример ответа пагинации (PageNumberPagination):
{"count": 123, "next": ".../?page=3", "previous": null, "results": [ ... ]}- PageNumberPagination: page, page_size (пример: /api/books/?page=2&page_size=50)
- LimitOffsetPagination: limit, offset (пример: /api/books/?limit=50&offset=100)
- Советы: ограничивайте максимальный размер страницы (max_page_size), не давайте запрашивать слишком большие выборки
Там мы можем указать тип класса пагинации и размер одной страницы, и все наши запросы уже будут пагинированы.
Также мы можем создать классы пагинаторов, основываясь на нашей необходимости.
from rest_framework.pagination import PageNumberPagination
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Если нужно указать пагинатор у конкретного вьюсета, то можно это сделать прямо в атрибутах.
from rest_framework import generics
class BillingRecordsView(generics.ListAPIView):
queryset = Billing.objects.all()
serializer_class = BillingRecordsSerializer
pagination_class = LargeResultsSetPaginationДокументация: https://www.django-rest-framework.org/api-guide/viewsets/#marking-extra-actions-for-routing
Что делать, если вам нужно дополнительное действие, связанное с деталями вашей вью, но ни один из CRUD не подходит? Тут
можно использовать декоратор @action, чтобы описать новое действие в этом же вьюсете.
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from blog.models import Article
from blog.serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
"""
ViewSet для статей с дополнительными действиями.
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""
Публикация статьи: POST /articles/{pk}/publish/
"""
article = self.get_object()
if article.status == 'published':
return Response(
{'error': 'Статья уже опубликована'},
status=status.HTTP_400_BAD_REQUEST
)
article.status = 'published'
article.save()
serializer = self.get_serializer(article)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def archive(self, request, pk=None):
"""
Архивирование статьи: POST /articles/{pk}/archive/
"""
article = self.get_object()
article.status = 'archived'
article.save()
return Response({'status': 'Статья архивирована'})
@action(detail=False, methods=['get'])
def published(self, request):
"""
Список опубликованных статей: GET /articles/published/
"""
published = self.get_queryset().filter(status='published')
page = self.paginate_queryset(published)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(published, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def my_articles(self, request):
"""
Статьи текущего пользователя: GET /articles/my_articles/
"""
my_articles = self.get_queryset().filter(author=request.user)
serializer = self.get_serializer(my_articles, many=True)
return Response(serializer.data)| Параметр | Описание |
|---|---|
detail |
True — действие над одним объектом (требует pk), False — над коллекцией |
methods |
Список HTTP методов: ['get'], ['post'], ['get', 'post'] |
url_path |
Кастомный путь URL (по умолчанию — имя метода) |
url_name |
Кастомное имя URL для reverse() |
permission_classes |
Отдельные permissions для этого action |
serializer_class |
Отдельный сериализатор для этого action |
Пример с кастомным URL:
@action(detail=True, methods=['post'], url_path='set-featured')
def set_featured(self, request, pk=None):
"""
URL будет: POST /articles/{pk}/set-featured/
"""
article = self.get_object()
article.is_featured = True
article.save()
return Response({'status': 'Статья добавлена в избранное'})Документация: https://www.django-rest-framework.org/api-guide/routers/
Роутер — это автоматический генератор URL-адресов для вьюсетов.
from rest_framework import routers
from blog.views import ArticleViewSet, TopicViewSet, CommentViewSet
router = routers.DefaultRouter()
router.register('articles', ArticleViewSet)
router.register('topics', TopicViewSet)
router.register('comments', CommentViewSet)
urlpatterns = router.urls| Роутер | Особенности |
|---|---|
SimpleRouter |
Базовый роутер, генерирует только URL-адреса для ViewSet |
DefaultRouter |
Добавляет API root (/api/) со списком всех эндпойнтов |
# DefaultRouter создаёт красивую главную страницу API-интерфейса
router = routers.DefaultRouter() # GET /api/ покажет все доступные эндпоинтыЕсли у ViewSet нет queryset, нужно указать basename:
class ReportViewSet(viewsets.ViewSet):
"""ViewSet без queryset — генерирует отчёты динамически."""
def list(self, request):
# Генерация отчётов
...
router = routers.DefaultRouter()
router.register('reports', ReportViewSet, basename='report')Для ArticleViewSet роутер создаст:
URL pattern: ^articles/$ Name: 'article-list'
URL pattern: ^articles/{pk}/$ Name: 'article-detail'
URL pattern: ^articles/published/$ Name: 'article-published' # @action
URL pattern: ^articles/{pk}/publish/$ Name: 'article-publish' # @action
from django.urls import path, include
from rest_framework import routers
from blog.views import ArticleViewSet, TopicViewSet
router = routers.DefaultRouter()
router.register('articles', ArticleViewSet)
router.register('topics', TopicViewSet)
urlpatterns = [
path('api/', include(router.urls)),
# Другие URL...
]Для @action декораторов роутер автоматически генерирует URL:
class ArticleViewSet(viewsets.ModelViewSet):
...
@action(methods=['post'], detail=True)
def publish(self, request, pk=None):
...Роутер создаст URL ^articles/{pk}/publish/$ и имя article-publish.
# По умолчанию URL-адреса заканчиваются на /
router = routers.SimpleRouter() # /articles/
# Можно отключить
router = routers.SimpleRouter(trailing_slash=False) # /articlesДля URL-адресов вида /articles/{article_pk}/comments/ можно использовать библиотеку drf-nested-routers:
pip install drf-nested-routersfrom rest_framework_nested import routers
from blog.views import ArticleViewSet, CommentViewSet
# Основной роутер
router = routers.DefaultRouter()
router.register('articles', ArticleViewSet)
# Вложенный роутер для комментариев
articles_router = routers.NestedDefaultRouter(router, 'articles', lookup='article')
articles_router.register('comments', CommentViewSet, basename='article-comments')
urlpatterns = [
path('api/', include(router.urls)),
path('api/', include(articles_router.urls)),
]Это создаст URL-адреса:
GET /api/articles/— список статейGET /api/articles/{pk}/— детали статьиGET /api/articles/{article_pk}/comments/— комментарии к статьеPOST /api/articles/{article_pk}/comments/— создать комментарий
ViewSet для вложенного ресурса:
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
def get_queryset(self):
return Comment.objects.filter(article_id=self.kwargs['article_pk'])
def perform_create(self, serializer):
serializer.save(
article_id=self.kwargs['article_pk'],
author=self.request.user
)Метод get_queryset() позволяет динамически фильтровать данные:
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
def get_queryset(self):
user = self.request.user
# Для списка — только опубликованные или свои
if self.action == 'list':
if user.is_authenticated:
# Авторизованный видит опубликованные + свои черновики
return Article.objects.filter(
Q(status='published') | Q(author=user)
)
# Анонимный видит только опубликованные
return Article.objects.filter(status='published')
# Для остальных действий — все статьи (проверка прав в permissions)
return Article.objects.all()Метод perform_create() вызывается при создании объекта:
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def perform_create(self, serializer):
"""Автоматически устанавливаем автора из request.user"""
serializer.save(author=self.request.user)class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def perform_update(self, serializer):
"""Сбрасываем статус при редактировании опубликованной статьи"""
instance = serializer.instance
if instance.status == 'published':
serializer.save(status='draft') # Требует повторной модерации
else:
serializer.save()class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def perform_destroy(self, instance):
"""Мягкое удаление вместо физического"""
instance.status = 'deleted'
instance.save()
# Или: instance.delete() для физического удаленияfrom django.db.models import Q
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from blog.models import Article
from blog.serializers import ArticleSerializer, ArticleListSerializer
class ArticleViewSet(viewsets.ModelViewSet):
"""
ViewSet для статей блога с полным CRUD и дополнительными действиями.
"""
permission_classes = [IsAuthenticatedOrReadOnly]
lookup_field = 'pk'
def get_queryset(self):
"""Фильтрация: опубликованные для всех, черновики только для автора."""
user = self.request.user
if self.action == 'list':
if user.is_authenticated:
return Article.objects.filter(
Q(status='published') | Q(author=user)
).select_related('author').prefetch_related('topics')
return Article.objects.filter(
status='published'
).select_related('author').prefetch_related('topics')
return Article.objects.all()
def get_serializer_class(self):
"""Разные сериализаторы для списка и деталей."""
if self.action == 'list':
return ArticleListSerializer
return ArticleSerializer
def perform_create(self, serializer):
"""Автоматически устанавливаем автора."""
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Публикация статьи."""
article = self.get_object()
if article.author != request.user:
return Response(
{'error': 'Только автор может публиковать статью'},
status=status.HTTP_403_FORBIDDEN
)
article.status = 'published'
article.save()
return Response(ArticleSerializer(article).data)
@action(detail=False, methods=['get'])
def my_drafts(self, request):
"""Черновики текущего пользователя."""
drafts = Article.objects.filter(author=request.user, status='draft')
serializer = ArticleListSerializer(drafts, many=True)
return Response(serializer.data)В этой лекции мы изучили:
-
Request и Response в DRF:
request.data,request.query_params, объектResponse -
@api_view: декоратор для функциональных представлений API
-
APIView: базовый класс для Class-Based Views в DRF
-
GenericAPIView и миксины: готовые CRUD-операции:
CreateModelMixin,ListModelMixin,RetrieveModelMixinUpdateModelMixin,DestroyModelMixin
-
Generic-классы:
ListCreateAPIView,RetrieveUpdateDestroyAPIViewи др. -
ViewSet и ModelViewSet: объединение всех CRUD в одном классе
-
@action: дополнительные действия (
publish,archive,my_drafts) -
Роутеры: автоматическая генерация URL-адресов для ViewSet
-
Вложенные роутеры: URL-адреса вида
/articles/{pk}/comments/ -
Переопределение методов:
get_queryset(),perform_create(),get_serializer_class()
- Создайте
ArticleViewSetс полным CRUD для статей блога - Добавьте
@actionдля публикации статьи - Настройте роутер и проверьте все эндпоинты через Browsable API
-
TopicViewSet (ReadOnlyModelViewSet):
- Только чтение тем
- Добавьте
@actionarticlesдля получения статей по теме
-
CommentViewSet:
- CRUD для комментариев
perform_create()— автоматически добавлять автораget_queryset()— фильтрация по статье из URL
-
Вложенный роутинг:
- Настройте URL
/articles/{pk}/comments/ - Используйте
drf-nested-routersили ручную настройку URL
- Настройте URL
-
Дополнительные @action для ArticleViewSet:
archive— архивирование статьиfeatured— добавление в избранноеby_topic— фильтрация по теме (query param?topic=python)
-
Пагинация:
- Создайте
ArticlePaginationсpage_size=10 - Примените к
ArticleViewSet
- Создайте
- В чём разница между
@api_viewиAPIView? - Какие миксины нужны для ViewSet только с
listиcreate? - Что делает параметр
detail=Trueв декораторе@action? - Чем
SimpleRouterотличается отDefaultRouter? - Когда нужно указывать
basenameпри регистрации ViewSet? - Как переопределить
get_queryset()для фильтрации по текущему пользователю? - Для чего используется
perform_create()? - Как создать URL вида
/articles/{pk}/comments/?
← Лекция 27: Что такое API. REST и RESTful. Django REST Framework. | Лекция 29: REST-аутентификация. Авторизация. Permissions. Фильтрация. →