🏠 Домашнее задание 19 — Урок 42

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

📋 3 задания 🎯 Сложность: Средняя–Высокая ⏱️ ~3–4 часа

⚡ Суть ДЗ 19

Цель: реализовать авторизацию с извлечением пользователя из запроса, разрешения на уровне объектов, интегрировать Swagger.

  1. Добавить поле owner в Task и SubTask, настроить perform_create, создать endpoint для задач текущего пользователя
  2. Создать кастомный permission IsOwnerOrReadOnly, применить к Task и SubTask
  3. Установить drf-yasg, настроить /swagger/ и /redoc/

Задание из LMS

Домашнее задание: Реализация JWT (SimpleJWT) аутентификации и пермишенов
Цель: Реализовать авторизацию с извлечением текущего пользователя из запроса и применение разрешений на уровне объектов. Настроить и интегрировать Swagger для автоматической генерации документации API.

Задание 1: Извлечение текущего пользователя из запроса

Шаги для выполнения:

  1. Обновите модели, чтобы включить поле owner.
    Обновите модели Task и SubTask для включения поля owner.
  2. Измените сериализаторы.
    Измените сериализаторы для моделей Task и SubTask для работы с новым полем.
  3. Переопределите метод perform_create в представлениях.
    Обновите представления для автоматического добавления владельца объекта.
  4. Создайте представления для получения задач текущего пользователя.
    Реализуйте представление для получения задач, принадлежащих текущему пользователю.

Задание 2: Реализация пермишенов для API

Шаги для выполнения:

  1. Создайте пользовательские пермишены.
    Реализуйте пользовательский пермишен для проверки, что пользователь является автором задачи или подзадачи.
  2. Примените пермишены к API представлениям.
    Добавьте пермишены к представлениям для задач и подзадач, чтобы только владельцы могли их изменять или удалять.

Задание 3: Swagger

Шаги для выполнения:

  1. Установите drf-yasg.
  2. Добавьте drf_yasg в settings.
  3. Настройте маршруты для Swagger в urls.py.
  4. Просмотр документации.
    Перейдите по URL /swagger/ или /redoc/, чтобы увидеть документацию для вашего API.

Оформление ответа

  • Предоставьте решение: прикрепите ссылку на git.
  • Скриншоты тестирования: приложите скриншоты из консоли или Postman, подтверждающие:
    • успешное извлечение текущего пользователя из запроса
    • соблюдение пермишенов при работе с задачами
    • реализованную документацию

Подготовка окружения

1. Создание виртуального окружения

# Windows PowerShell
cd your-django-project
python -m venv venv
.\venv\Scripts\Activate.ps1

2. Установка зависимостей

pip install django djangorestframework djangorestframework-simplejwt drf-yasg
pip freeze > requirements.txt

3. Структура проекта

myproject/
  myproject/
    settings.py
    urls.py
  tasks/
    models.py
    serializers.py
    views.py
    permissions.py
    urls.py
  manage.py
  requirements.txt

4. Git инициализация

git init
git add .
git commit -m "initial: project setup"

Пошаговое решение

Шаг 1: Настройка settings.py

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'drf_yasg',        # Swagger
    'tasks',           # ваше приложение
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
}

Связь с теорией: см. раздел «request.user» и Урок 40: JWT-аутентификация.

Шаг 2: Обновление models.py — добавление owner

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

class Task(models.Model):
    STATUS_CHOICES = [
        ('New', 'New'),
        ('In progress', 'In progress'),
        ('Done', 'Done'),
    ]
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='New')
    deadline = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='tasks'
        # Не используем null=True — новые записи обязаны иметь владельца
    )

    def __str__(self):
        return f"{self.title} ({self.status})"


class SubTask(models.Model):
    STATUS_CHOICES = Task.STATUS_CHOICES
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='subtasks')
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='New')
    deadline = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='subtasks'
    )

    def __str__(self):
        return f"{self.title} ({self.status})"
python manage.py makemigrations tasks
python manage.py migrate
При миграции Django предложит задать дефолтное значение для поля owner в существующих строках. Введите 1 (или id любого существующего пользователя). Если у вас нет пользователей — создайте суперпользователя: python manage.py createsuperuser.

Шаг 3: Сериализаторы с read_only owner

# tasks/serializers.py
from rest_framework import serializers
from .models import Task, SubTask

class SubTaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = SubTask
        fields = '__all__'
        read_only_fields = ['owner', 'created_at']

class TaskSerializer(serializers.ModelSerializer):
    subtasks = SubTaskSerializer(many=True, read_only=True)

    class Meta:
        model = Task
        fields = '__all__'
        read_only_fields = ['owner', 'created_at']

Связь с теорией: Теория — автоматическое назначение владельца объясняет, зачем owner должен быть read_only.

Шаг 4: Кастомный permission

# tasks/permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS

class IsOwnerOrReadOnly(BasePermission):
    """
    Разрешает изменение объектов только их владельцам.
    Остальным — только чтение (SAFE_METHODS).
    """
    message = "Изменять или удалять объект может только его владелец."

    def has_object_permission(self, request, view, obj):
        # Чтение разрешено всем аутентифицированным
        if request.method in SAFE_METHODS:
            return True
        # Изменение/удаление — только владелец
        return obj.owner == request.user

Связь с теорией: Теория — разрешения на уровне объектов.

Шаг 5: Представления с perform_create и get_queryset

# tasks/views.py
from rest_framework import viewsets
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from .models import Task, SubTask
from .serializers import TaskSerializer, SubTaskSerializer
from .permissions import IsOwnerOrReadOnly

class TaskViewSet(viewsets.ModelViewSet):
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

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


class SubTaskViewSet(viewsets.ModelViewSet):
    queryset = SubTask.objects.all()
    serializer_class = SubTaskSerializer
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

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


class UserTaskListView(ListAPIView):
    """Возвращает только задачи текущего пользователя."""
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Task.objects.filter(owner=self.request.user)


class UserSubTaskListView(ListAPIView):
    """Возвращает только подзадачи текущего пользователя."""
    serializer_class = SubTaskSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return SubTask.objects.filter(owner=self.request.user)

Шаг 6: Настройка Swagger (drf-yasg)

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework import permissions
from tasks.views import TaskViewSet, SubTaskViewSet, UserTaskListView, UserSubTaskListView

# Swagger схема
schema_view = get_schema_view(
    openapi.Info(
        title="Tasks API",
        default_version='v1',
        description="API для управления задачами с JWT-аутентификацией",
        contact=openapi.Contact(email="admin@example.com"),
    ),
    public=True,
    permission_classes=[permissions.AllowAny],
)

router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='task')
router.register(r'subtasks', SubTaskViewSet, basename='subtask')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    path('api/my-tasks/', UserTaskListView.as_view(), name='my-tasks'),
    path('api/my-subtasks/', UserSubTaskListView.as_view(), name='my-subtasks'),

    # JWT токены
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

    # Swagger документация
    path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
    path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
]

Проверка в VS Code

1. Запуск сервера в терминале

# В терминале VS Code (Ctrl+`)
.\venv\Scripts\Activate.ps1
python manage.py runserver

2. Запуск через F5 (launch.json)

Создайте файл .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Django: Run Server",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/manage.py",
            "args": ["runserver"],
            "django": true,
            "env": {
                "PYTHONPATH": "${workspaceFolder}"
            }
        }
    ]
}

Нажмите F5 для запуска. Точки останова (F9) можно ставить в любой файл проекта.

3. Точки останова для отладки permissions

Поставьте точку останова на строку return obj.owner == request.user в permissions.py. В VS Code в режиме отладки (F5) при PUT-запросе точка сработает и можно проверить переменные obj.owner, request.user.

4. Проверка в Postman

# Шаг 1: Получить JWT токен
POST http://127.0.0.1:8000/api/token/
Body: {"username": "your_user", "password": "your_password"}
# Response: {"access": "...", "refresh": "..."}

# Шаг 2: Создать задачу (owner автоматически = текущий user)
POST http://127.0.0.1:8000/api/tasks/
Header: Authorization: Bearer <access_token>
Body: {"title": "My Task", "description": "Test"}
# Response: {"id": 1, "title": "My Task", "owner": 1, ...}

# Шаг 3: Попробовать изменить задачу другим пользователем
PUT http://127.0.0.1:8000/api/tasks/1/
Header: Authorization: Bearer <токен_другого_пользователя>
# Response 403: {"detail": "Изменять или удалять объект может только его владелец."}

# Шаг 4: Получить только свои задачи
GET http://127.0.0.1:8000/api/my-tasks/
Header: Authorization: Bearer <мой_токен>
# Response: [только мои задачи]

# Шаг 5: Открыть Swagger
# Браузер: http://127.0.0.1:8000/swagger/
# Браузер: http://127.0.0.1:8000/redoc/