APIView, GenericAPIView, ViewSet, Router, pagination, filtering
DRF предоставляет мощные абстракции для создания API view: от низкоуровневых APIView до высокоуровневых ViewSets с автоматической маршрутизацией.
💡 Правило: Используйте ViewSets для стандартных CRUD операций. Используйте APIView для кастомной логики которая не вписывается в CRUD.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Book
from .serializers import BookSerializer
class BookList(APIView):
"""Список книг и создание новой."""
def get(self, request):
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response(serializer.data)
def post(self, request):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BookDetail(APIView):
"""Детальный просмотр, обновление, удаление книги."""
def get_object(self, pk):
try:
return Book.objects.get(pk=pk)
except Book.DoesNotExist:
return None
def get(self, request, pk):
book = self.get_object(pk)
if book is None:
return Response({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)
serializer = BookSerializer(book)
return Response(serializer.data)
def put(self, request, pk):
book = self.get_object(pk)
if book is None:
return Response({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)
serializer = BookSerializer(book, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk):
book = self.get_object(pk)
if book is None:
return Response({'error': 'Not found'}, status=status.HTTP_404_NOT_FOUND)
book.delete()
return Response(status=status.HTTP_204_NO_CONTENT)from django.urls import path
from .views import BookList, BookDetail
urlpatterns = [
path('books/', BookList.as_view()),
path('books/<int:pk>/', BookDetail.as_view()),
]from rest_framework import generics
from .models import Book
from .serializers import BookSerializer
class BookList(generics.ListCreateAPIView):
"""Список и создание — готовый view."""
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookDetail(generics.RetrieveUpdateDestroyAPIView):
"""Детальный просмотр, обновление, удаление — готовый view."""
queryset = Book.objects.all()
serializer_class = BookSerializerfrom rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Book
from .serializers import BookSerializer
class BookList(generics.ListCreateAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
"""Фильтрация по параметрам."""
queryset = super().get_queryset()
# Фильтр по автору
author = self.request.query_params.get('author')
if author:
queryset = queryset.filter(author__icontains=author)
# Фильтр по цене
min_price = self.request.query_params.get('min_price')
if min_price:
queryset = queryset.filter(price__gte=min_price)
return queryset
def perform_create(self, serializer):
"""Дополнительная логика при создании."""
serializer.save(created_by=self.request.user)
class BookDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
"""Только опубликованные книги для неавторизованных."""
queryset = super().get_queryset()
if not self.request.user.is_authenticated:
queryset = queryset.filter(is_published=True)
return querysetfrom rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
"""Полный CRUD для книг."""
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
# Доступные действия:
# list() — GET /books/
# create() — POST /books/
# retrieve() — GET /books/{id}/
# update() — PUT /books/{id}/
# partial_update() — PATCH /books/{id}/
# destroy() — DELETE /books/{id}/from rest_framework import viewsets
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""Только чтение для категорий."""
queryset = Category.objects.all()
serializer_class = CategorySerializer
# Только list() и retrieve()from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer, PostDetailSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.filter(is_published=True)
permission_classes = [IsAuthenticatedOrReadOnly]
def get_serializer_class(self):
"""Разные сериализаторы для разных действий."""
if self.action == 'list':
return PostListSerializer
if self.action in ['retrieve', 'create', 'update']:
return PostDetailSerializer
return PostSerializer
def get_queryset(self):
"""Фильтрация по параметрам."""
queryset = super().get_queryset()
# Фильтр по категории
category = self.request.query_params.get('category')
if category:
queryset = queryset.filter(category__slug=category)
# Фильтр по автору
author = self.request.query_params.get('author_id')
if author:
queryset = queryset.filter(author_id=author)
# Поиск
search = self.request.query_params.get('search')
if search:
queryset = queryset.filter(title__icontains=search)
return queryset
def perform_create(self, serializer):
"""Установить автора при создании."""
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Custom action: опубликовать пост."""
post = self.get_object()
# Проверка прав
if post.author != request.user and not request.user.is_staff:
return Response(
{'error': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN
)
post.is_published = True
post.save()
return Response({'status': 'published'})
@action(detail=True, methods=['post'])
def like(self, request, pk=None):
"""Custom action: лайкнуть пост."""
post = self.get_object()
if request.user.is_authenticated:
post.likes.add(request.user)
return Response({'status': 'liked'})
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
@action(detail=True, methods=['post'])
def unlike(self, request, pk=None):
"""Custom action: убрать лайк."""
post = self.get_object()
if request.user.is_authenticated:
post.likes.remove(request.user)
return Response({'status': 'unliked'})
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
@action(detail=False)
def my_posts(self, request):
"""Custom action: мои посты."""
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
posts = self.queryset.filter(author=request.user)
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CategoryViewSet, UserViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urlsСозданные URL:
^posts/$ name='post-list'
^posts/{pk}/$ name='post-detail'
^posts/{pk}/publish/$ name='post-publish'
^posts/{pk}/like/$ name='post-like'
^posts/my_posts/$ name='post-my-posts'
^categories/$ name='category-list'
^categories/{pk}/$ name='category-detail'
^users/$ name='user-list'
^users/{pk}/$ name='user-detail'
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register(r'posts', PostViewSet)
# Меньше функций чем DefaultRouter (нет API root)# settings.py
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}from rest_framework.pagination import PageNumberPagination, LimitOffsetPagination, CursorPagination
class StandardPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class CustomLimitOffsetPagination(LimitOffsetPagination):
default_limit = 10
limit_query_param = 'limit'
offset_query_param = 'offset'
max_limit = 100
class CustomCursorPagination(CursorPagination):
cursor_query_param = 'cursor'
page_size = 10
ordering = '-created_at' # Обязательно для cursor pagination
# В ViewSet
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
pagination_class = StandardPagination # Или None для отключенияОтвет с пагинацией:
{
"count": 100,
"next": "http://api/posts/?page=2",
"previous": null,
"results": [...]
}# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
]
}from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
# Точная фильтрация
filterset_fields = ['category', 'is_published', 'author']
# Поиск по тексту
search_fields = ['title', 'content', 'author__username']
search_fields = ['^title', '=author__username', '@content'] # starts, exact, full-text
# Сортировка
ordering_fields = ['created_at', 'title', 'views', 'comment_count']
ordering = ['-created_at'] # По умолчаниюПримеры запросов:
GET /posts/?category=django&is_published=true
GET /posts/?search=django
GET /posts/?ordering=-created_at,title
GET /posts/?author=1&search=python&ordering=views
# blog/api/views.py
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from ..models import Post, Category, Tag, Comment
from .serializers import (
PostListSerializer, PostDetailSerializer, PostCreateUpdateSerializer,
CategorySerializer, TagSerializer, CommentSerializer
)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""API для категорий — только чтение."""
queryset = Category.objects.all()
serializer_class = CategorySerializer
filter_backends = [SearchFilter]
search_fields = ['name']
class TagViewSet(viewsets.ReadOnlyModelViewSet):
"""API для тегов — только чтение."""
queryset = Tag.objects.all()
serializer_class = TagSerializer
filter_backends = [SearchFilter]
search_fields = ['name']
class CommentViewSet(viewsets.ModelViewSet):
"""API для комментариев."""
queryset = Comment.objects.filter(is_approved=True)
serializer_class = CommentSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
queryset = super().get_queryset()
post_id = self.request.query_params.get('post')
if post_id:
queryset = queryset.filter(post_id=post_id)
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class PostViewSet(viewsets.ModelViewSet):
"""Полный API для постов."""
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['category', 'author', 'is_published']
search_fields = ['title', 'content', 'tags__name']
ordering_fields = ['created_at', 'views', 'comment_count']
ordering = ['-created_at']
def get_queryset(self):
queryset = Post.objects.filter(is_published=True)
# Для авторизованных показывать черновики автора
if self.request.user.is_authenticated:
user = self.request.user
queryset = Post.objects.filter(
models.Q(is_published=True) |
models.Q(author=user)
)
return queryset
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
if self.action in ['create', 'update', 'partial_update']:
return PostCreateUpdateSerializer
return PostDetailSerializer
def get_permissions(self):
"""Разные права для разных действий."""
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [permissions.IsAuthenticated()]
return [permissions.IsAuthenticatedOrReadOnly()]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
"""Опубликовать пост (только автор или админ)."""
post = self.get_object()
if post.author != request.user and not request.user.is_staff:
return Response(
{'error': 'Permission denied'},
status=status.HTTP_403_FORBIDDEN
)
post.is_published = True
post.save()
return Response({'status': 'published'})
@action(detail=True, methods=['post', 'delete'])
def like(self, request, pk=None):
"""Лайкнуть/убрать лайк."""
post = self.get_object()
if not request.user.is_authenticated:
return Response(
{'error': 'Authentication required'},
status=status.HTTP_401_UNAUTHORIZED
)
if request.method == 'POST':
post.likes.add(request.user)
return Response({'status': 'liked'})
else:
post.likes.remove(request.user)
return Response({'status': 'unliked'})
@action(detail=False)
def trending(self, request):
"""Трендовые посты (по просмотрам и комментариям)."""
posts = self.get_queryset().annotate(
engagement=models.Count('comments') + models.Count('likes') * 2
).order_by('-engagement', '-views')[:10]
serializer = self.get_serializer(posts, many=True)
return Response(serializer.data)# blog/api/urls.py
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CategoryViewSet, TagViewSet, CommentViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'tags', TagViewSet, basename='tag')
router.register(r'comments', CommentViewSet, basename='comment')
urlpatterns = router.urlsВопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.