🏠 Самостоятельная практика

Закрепляющее задание по темам практикума 10

Не LMS-задание. Это закрепляющее задание составлено на основе тем практикума 10. Выполняется самостоятельно для углублённой практики.

⚡ Задание кратко

Расширить DRF-проект блогом: авто-привязка автора, IsAuthorOrReadOnly, кастомное разрешение can_publish, Swagger-документация.

  1. Модель Post с полем author (FK на User), Meta.permissions = [("can_publish", "Can publish posts")]
  2. ViewSet с perform_createauthor=request.user и get_queryset → фильтр по автору
  3. Класс IsAuthorOrReadOnly(BasePermission) — только автор редактирует
  4. Класс CanPublish(BasePermission) — проверка blog.can_publish
  5. Swagger через drf-yasg: /swagger/ и /redoc/
  6. Дамп: dumpdata --indent=4 > blog_backup.json

Задание: Блог с кастомными разрешениями и Swagger

Создайте DRF-проект блога (или расширьте существующий проект store) с реализацией всех паттернов практикума 10.

Часть 1: Подготовка окружения

# Создать виртуальное окружение
python -m venv blog_env

# Активировать (Windows)
blog_env\Scripts\activate

# Установить зависимости
pip install django djangorestframework drf-yasg

# Создать проект
django-admin startproject blog_project .
python manage.py startapp blog

# Применить миграции Django
python manage.py migrate

# Создать суперпользователя
python manage.py createsuperuser

Часть 2: Модель с пользовательским разрешением

Создайте модель Post с авто-привязкой автора и кастомным разрешением:

# blog/models.py
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    published = models.BooleanField(default=False)

    class Meta:
        permissions = [
            ("can_publish", "Can publish posts"),
        ]

    def __str__(self):
        return self.title
python manage.py makemigrations
python manage.py migrate

Часть 3: Кастомные разрешения

Создайте файл blog/permissions.py:

# blog/permissions.py
from rest_framework.permissions import BasePermission

class IsAuthorOrReadOnly(BasePermission):
    """
    Редактирование — только автор поста,
    чтение — все аутентифицированные.
    """
    def has_object_permission(self, request, view, obj):
        if request.method in ['GET', 'HEAD', 'OPTIONS']:
            return True
        return obj.author == request.user


class CanPublish(BasePermission):
    """
    Публикация поста — только пользователи с разрешением 'blog.can_publish'.
    """
    def has_permission(self, request, view):
        return request.user.has_perm('blog.can_publish')

Часть 4: Представления

# blog/views.py
from rest_framework import viewsets
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Post
from .serializers import PostSerializer
from .permissions import IsAuthorOrReadOnly, CanPublish


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated, IsAuthorOrReadOnly]

    def perform_create(self, serializer):
        # Автор берётся из запроса автоматически
        serializer.save(author=self.request.user)

    @action(detail=True, methods=['post'],
            permission_classes=[IsAuthenticated, CanPublish])
    def publish(self, request, pk=None):
        post = self.get_object()
        post.published = True
        post.save()
        return Response({'status': 'published'})


class MyPostListView(ListAPIView):
    """Только посты текущего пользователя."""
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Post.objects.filter(author=self.request.user)

Часть 5: Маршруты и Swagger

# blog_project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from blog.views import PostViewSet, MyPostListView

router = DefaultRouter()
router.register(r'posts', PostViewSet)

schema_view = get_schema_view(
    openapi.Info(
        title="Blog API",
        default_version='v1',
        description="API for blog with custom permissions",
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    path('api/my-posts/', MyPostListView.as_view(), name='my-posts'),
    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0)),
    path('redoc/', schema_view.with_ui('redoc', cache_timeout=0)),
]

Часть 6: Резервная копия базы данных

# Создать несколько постов через API или Admin, затем:
python manage.py dumpdata --indent=4 > blog_backup.json

# Проверить файл
# Удалить DB (db.sqlite3)
# Восстановить:
python manage.py migrate
python manage.py loaddata blog_backup.json

Проверка в VS Code и Postman

Запуск сервера через VS Code

  1. Откройте папку проекта в VS Code
  2. Создайте .vscode/launch.json:
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Django",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/manage.py",
      "args": ["runserver"],
      "django": true,
      "justMyCode": true
    }
  ]
}
  1. Нажмите F5 для запуска с отладчиком
  2. Установите точку останова в perform_create — убедитесь, что self.request.user содержит правильного пользователя

Проверка через Postman

  1. Откройте http://127.0.0.1:8000/swagger/ — должна открыться Swagger UI
  2. В Postman: POST /api/posts/ с JWT-токеном → убедитесь, что author заполнился автоматически
  3. GET /api/my-posts/ — только посты текущего пользователя
  4. PUT /api/posts/{id}/ от другого пользователя → 403 Forbidden
  5. POST /api/posts/{id}/publish/ без can_publish → 403; с разрешением → 200

Связь с разделами урока

← К оглавлению урока