✅ Решения практикума 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: Настройка Swagger

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

pip install drf-yasg

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

# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'drf_yasg',
]
Объяснение: drf_yasg нужно добавить в INSTALLED_APPS, чтобы Django нашёл статику и шаблоны библиотеки (Swagger UI — это набор JS/CSS файлов).

Решение 5.3 — Маршруты для Swagger

# urls.py
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi

schema_view = get_schema_view(
    openapi.Info(
        title="API Documentation",
        default_version='v1',
        description="API documentation for the project",
        terms_of_service="https://www.google.com/policies/terms/",
        contact=openapi.Contact(email="contact@local.com"),
        license=openapi.License(name="BSD License"),
    ),
    public=True,
    permission_classes=(permissions.AllowAny,),
)

urlpatterns += [
    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'),
]
Объяснение:
  • get_schema_view() — создаёт объект представления схемы с метаданными API
  • public=True — схема доступна без аутентификации
  • permission_classes=(permissions.AllowAny,) — документация открыта для всех
  • with_ui('swagger') — рендерит Swagger UI (интерактивная документация)
  • with_ui('redoc') — рендерит ReDoc (читаемая документация)
  • cache_timeout=0 — отключает кэширование схемы (удобно при разработке)
⚠️ Проверить по документации: drf-yasg поддерживает DRF до 3.14. Для более новых версий DRF рекомендуется проверить совместимость или использовать drf-spectacular.
← К оглавлению урока    Ошибки →