Итоги работы

⏱ 15 мин · 🧩 заданий: 5

Итоговый код API

blog/api/serializers.py:

from rest_framework import serializers

from ..models import Comment, Post


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Comment
        fields = ["id", "post", "name", "body", "created"]
        read_only_fields = ["created"]


class PostSerializer(serializers.ModelSerializer):
    author = serializers.ReadOnlyField(source="author.username")
    tags = serializers.SlugRelatedField(many=True, read_only=True, slug_field="name")
    comments_count = serializers.SerializerMethodField()

    class Meta:
        model = Post
        fields = ["id", "title", "slug", "author", "body", "publish", "status", "tags", "comments_count"]

    def get_comments_count(self, obj):
        return obj.comments.count()

blog/api/views.py:

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters, permissions, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from ..models import Comment, Post
from .permissions import IsAuthorOrReadOnly
from .serializers import CommentSerializer, PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    """Посты блога. Читать — всем, создавать — вошедшим, изменять — автору."""

    serializer_class = PostSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ["status", "author"]
    search_fields = ["title", "body"]
    ordering_fields = ["publish", "title"]

    def get_queryset(self):
        qs = Post.objects.select_related("author").prefetch_related("tags")
        if self.action in ("list", "retrieve") and not self.request.user.is_staff:
            qs = qs.filter(status=Post.Status.PUBLISHED)
        return qs

    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()
        post.status = Post.Status.PUBLISHED
        post.save(update_fields=["status"])
        return Response({"status": "published"})


class CommentViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Comment.objects.filter(active=True)
    serializer_class = CommentSerializer
    filterset_fields = ["post"]

blog/api/urls.py:

from django.urls import include, path
from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.routers import DefaultRouter

from . import views

router = DefaultRouter()
router.register("posts", views.PostViewSet, basename="post")
router.register("comments", views.CommentViewSet)

urlpatterns = [
    path("", include(router.urls)),
    path("token/", obtain_auth_token),
]

Обратите внимание на basename="post": у PostViewSet нет атрибута queryset (только get_queryset()), поэтому роутеру нужно явно указать базовое имя маршрутов.

settings.py:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticatedOrReadOnly"],
    "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
    "DEFAULT_THROTTLE_CLASSES": ["rest_framework.throttling.AnonRateThrottle"],
    "DEFAULT_THROTTLE_RATES": {"anon": "1000/day"},
}

Чек-лист

  • Понимаю принципы REST и коды ответов.
  • Умею писать ModelSerializer со связями, вычисляемыми полями и валидацией.
  • Знаю разницу между APIView, generic views и ViewSet.
  • Умею добавить фильтры, поиск, сортировку и пагинацию.
  • Понимаю, как работают права и токены, и могу написать своё право.
  • Умею сгенерировать документацию.

Что дальше

Блог и его API готовы — пора показать их миру. Следующий модуль — деплой на VPS: сервер, домен, Gunicorn, PostgreSQL, NGINX и HTTPS.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Расставить по порядку

Запрос PATCH /api/posts/5/ с токеном. Расставьте этапы его обработки в DRF.

  1. Классы аутентификации определяют request.user по токену
  2. get_object() находит пост и проверяет has_object_permission
  3. Сериализатор валидирует request.data и сохраняет изменения
  4. Проверяются permission_classes (сначала has_permission)
  5. Возвращается Response с обновлённым постом и кодом 200
  6. Роутер направляет запрос в PostViewSet.partial_update
Задание 2 Один вариант ответа

Анонимный пользователь отправил DELETE /api/posts/5/ при правах IsAuthenticatedOrReadOnly и сессионной аутентификации. Что он получит?

Задание 3 Написать код

Контрольное задание: CommentViewSet только для чтения (ReadOnlyModelViewSet) с набором активных комментариев Comment.objects.filter(active=True), сериализатором CommentSerializer и фильтрацией по полю post через filterset_fields.

Задание 4 Несколько вариантов

Какие пакеты мы использовали в этом модуле?

Задание 5 Практика (выполнить у себя)

Соберите API блога: откройте /api/ (корень роутера), создайте пост с токеном через curl, отфильтруйте посты ?search=, проверьте пагинацию и откройте /api/docs/. Нажмите, когда всё заработает.

Войдите, чтобы отмечать пройденные уроки.