✅ Решения практикума 10

Полный разбор всех 5 заданий с кодом и объяснением по источнику PrfS10

⚡ Все решения — быстрые ссылки

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

Решение 1.1 — Автоматическое добавление клиента при оформлении заказа

# views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Order
from .serializers import OrderSerializer

class OrderListCreateView(viewsets.ModelViewSet):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer
    permission_classes = [IsAuthenticated]

    def perform_create(self, serializer):
        serializer.save(customer=self.request.user)
Объяснение: Метод perform_create() вызывается после валидации данных и до сохранения в БД. Передача customer=self.request.user в serializer.save() переопределяет значение поля — пользователь не может подставить чужой customer, даже если отправит его в теле запроса.

Решение 1.2 — Извлечение объектов, где пользователь является владельцем

# views.py
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated
from .models import Order
from .serializers import OrderSerializer

class UserOrderListView(ListAPIView):
    serializer_class = OrderSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Order.objects.filter(customer=self.request.user)
Объяснение: Переопределение get_queryset() гарантирует, что пользователь видит только свои заказы. Без этой фильтрации пользователь мог бы запросить чужой заказ по ID напрямую — это нарушение приватности данных.

Решение 1.3 — Обновление маршрутов

# urls.py
from django.urls import path
from .views import UserOrderListView, OrderListCreateView

urlpatterns = [
    path('user-orders/', UserOrderListView.as_view(), name='user-orders'),
    path('orders/', OrderListCreateView.as_view({'get': 'list', 'post': 'create'}), name='orders'),
]
Объяснение: UserOrderListView — только список заказов текущего пользователя. OrderListCreateView — создание заказа (customer берётся из запроса автоматически). Два разных маршрута решают разные задачи.

Задание 2: Создание кастомных классов разрешений

Решение 2.1 — IsCustomerOrReadOnly

# permissions.py
from rest_framework.permissions import BasePermission

class IsCustomerOrReadOnly(BasePermission):
    """
    Разрешает редактирование объектов только их владельцам (customer),
    остальным — только чтение.
    """
    def has_object_permission(self, request, view, obj):
        # Все пользователи могут просматривать
        if request.method in ['GET', 'HEAD', 'OPTIONS']:
            return True
        # Только владелец может изменять объект
        return obj.customer == request.user
Объяснение: has_object_permission вызывается для каждого конкретного объекта (при retrieve/update/destroy). Метод возвращает True для безопасных HTTP-методов (чтение) и проверяет владельца для небезопасных (запись/удаление). Это классический паттерн "IsOwnerOrReadOnly".

Решение 2.2 — Применение IsCustomerOrReadOnly к представлению

# views.py
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from .models import Order
from .serializers import OrderSerializer
from .permissions import IsCustomerOrReadOnly

class OrderDetailUpdateDeleteView(RetrieveUpdateDestroyAPIView):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer
    permission_classes = [IsCustomerOrReadOnly]
Объяснение: RetrieveUpdateDestroyAPIView обрабатывает GET (один объект), PUT/PATCH (обновление), DELETE (удаление). При PUT/PATCH/DELETE DRF вызывает has_object_permission — там происходит проверка владельца. Анонимный пользователь и аутентифицированный не-владелец получат 403.

Задание 3: Добавление эндпоинта для статистики

Решение 3.1 — Пользовательское разрешение в Meta

# models.py
from django.db import models

class Order(models.Model):
    order_date = models.DateTimeField(auto_now_add=True)
    customer = models.ForeignKey(Customer, on_delete=models.PROTECT,
                                  related_name='orders')

    class Meta:
        permissions = [
            ("can_view_statistics", "Can view statistics"),
        ]
Объяснение: Meta.permissions регистрирует нестандартное разрешение Django. Формат: кортеж ("codename", "Human-readable name"). После выполнения миграций разрешение появляется в таблице auth_permission и доступно в Admin.

Решение 3.2 — Кастомное разрешение CanViewStatistics

# permissions.py
from rest_framework.permissions import BasePermission

class CanViewStatistics(BasePermission):
    """
    Разрешает доступ к статистике только пользователям с соответствующим
    разрешением.
    """
    def has_permission(self, request, view):
        return request.user.has_perm('store.can_view_statistics')
Объяснение: has_perm('store.can_view_statistics') проверяет разрешение вида 'app_label.codename'. Здесь store — это название приложения Django, can_view_statistics — codename из Meta.permissions. Разрешение может быть присвоено пользователю напрямую или через группу.

Решение 3.3 — Эндпоинт для статистики

# views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from .permissions import CanViewStatistics
from .models import Order

class OrderStatisticsView(APIView):
    permission_classes = [CanViewStatistics]

    def get(self, request, *args, **kwargs):
        total_orders = Order.objects.count()
        data = {
            'total_orders': total_orders,
        }
        return Response(data)
Объяснение: APIView — базовый класс DRF для произвольных эндпоинтов без привязки к модели. Метод get() обрабатывает GET-запросы. permission_classes = [CanViewStatistics] защищает эндпоинт: без нужного разрешения пользователь получит 403.

Решение 3.4 — Маршрут для статистики

# urls.py
from .views import OrderStatisticsView

urlpatterns += [
    path('order-statistics/', OrderStatisticsView.as_view(), name='order-statistics'),
]

Решение 3.5 — Миграции

python manage.py makemigrations
python manage.py migrate
Обязательно! После добавления Meta.permissions необходимо создать и применить миграции. Без этого разрешение не появится в БД и в Django Admin.

Решение 3.6 — Группа в Django Admin

  1. Зайдите в http://127.0.0.1:8000/admin/
  2. Перейдите в раздел Groups (Группы)
  3. Создайте группу "Statistic"
  4. Добавьте в группу разрешение "Can view statistics" (ищите в фильтре)
  5. Сохраните группу
  6. Перейдите в раздел Users (Пользователи)
  7. Найдите нужного пользователя (первый admin)
  8. В разделе Groups добавьте группу "Statistic"
  9. Сохраните пользователя

Задание 4: Управление базой данных

Решение 4.1 — Создание дампа базы данных

# Создать дамп всех объектов проекта
python manage.py dumpdata --indent=4 > db_backup.json
Объяснение: dumpdata экспортирует все данные из всех приложений в формате JSON. Флаг --indent=4 делает файл читаемым (4 пробела). Результат сохраняется в db_backup.json в корне проекта.

Решение 4.2 — Восстановление базы данных из дампа

# После удаления БД: применить миграции (создать структуру таблиц)
python manage.py migrate

# Загрузить данные из дампа
python manage.py loaddata db_backup.json
Объяснение: migrate создаёт пустые таблицы согласно миграциям. loaddata наполняет их данными из JSON-файла. Порядок важен: сначала структура, потом данные.

Задание 5: Настройка OpenAPI 3 документации

Решение 5.1 — Установка библиотеки

pip install drf-spectacular

Решение 5.2 — Настройка в settings.py

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'drf_spectacular',
]

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'API Documentation',
    'DESCRIPTION': 'API documentation for the project',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': False,
}
Объяснение: drf_spectacular подключается как Django-приложение, а DEFAULT_SCHEMA_CLASS говорит DRF использовать OpenAPI 3 генератор вместо встроенной схемы.

Решение 5.3 — Маршруты для схемы и UI

# urls.py
from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularRedocView,
    SpectacularSwaggerView,
)

urlpatterns += [
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
Объяснение:
  • SpectacularAPIView — отдаёт машинно-читаемую OpenAPI 3 схему
  • SpectacularSwaggerView — рендерит Swagger UI поверх этой схемы
  • SpectacularRedocView — рендерит ReDoc поверх той же схемы
  • url_name='schema' — связывает UI с маршрутом схемы
  • SPECTACULAR_SETTINGS — хранит title, description и version API
← К оглавлению урока    Ошибки →