🐛 Типичные ошибки — Урок 42

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

⚡ Топ-3 ошибки урока

  1. has_object_permission не вызывается для list — защита списка через get_queryset(), не через permissions
  2. owner не read_only в сериализаторе — клиент может подменить владельца; добавьте в read_only_fields
  3. Забыли добавить IsAuthenticatedhas_object_permission получает AnonymousUser, что ломает сравнение obj.owner == request.user

Ошибка 1: has_object_permission не защищает list-эндпоинт

Самая частая ошибка: разработчик добавляет IsOwnerOrReadOnly в permission_classes ViewSet и думает, что список книг тоже защищён. Это не так.
# НЕПРАВИЛЬНО: has_object_permission НЕ вызывается при GET /books/
class BookViewSet(viewsets.ModelViewSet):
    permission_classes = [IsOwnerOrReadOnly]
    # Любой может увидеть список всех книг!
# ПРАВИЛЬНО: фильтрация в get_queryset()
class BookViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

    def get_queryset(self):
        # Для list — только книги текущего пользователя
        return Book.objects.filter(owner=self.request.user)

Ошибка 2: owner не помечен как read_only

# НЕПРАВИЛЬНО: owner можно подменить в запросе
class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
        # Нет read_only_fields!

# ПРАВИЛЬНО
class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'
        read_only_fields = ['owner']  # клиент не может изменить owner

Ошибка 3: Отсутствие IsAuthenticated при кастомном permission

# НЕПРАВИЛЬНО: анонимный пользователь вызовет AttributeError
class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        return obj.owner == request.user  # request.user = AnonymousUser!

class BookView(RetrieveUpdateDestroyAPIView):
    permission_classes = [IsOwnerOrReadOnly]  # нет IsAuthenticated!
# ПРАВИЛЬНО: сначала проверяем аутентификацию
class BookView(RetrieveUpdateDestroyAPIView):
    permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]

Ошибка 4: Неправильное место проверки in has_permission vs has_object_permission

# НЕПРАВИЛЬНО: проверка владельца в has_permission (нет доступа к объекту)
class IsOwner(BasePermission):
    def has_permission(self, request, view):
        # obj недоступен здесь!
        return request.user == view.get_object().owner  # рекурсия или N+1 запросов
# ПРАВИЛЬНО: проверка владельца в has_object_permission
class IsOwnerOrReadOnly(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True
        return obj.owner == request.user  # obj здесь доступен

Ошибка 5: DjangoModelPermissions без queryset

# НЕПРАВИЛЬНО: DjangoModelPermissions требует queryset для определения модели
class BookView(APIView):
    permission_classes = [DjangoModelPermissions]
    # AssertionError: Cannot use DjangoModelPermissions without a queryset
# ПРАВИЛЬНО: добавьте queryset или get_queryset
class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()  # обязательно
    permission_classes = [DjangoModelPermissions]

Ошибка 6: Миграция поля owner без null=True

# СИТУАЦИЯ: добавляем owner в существующую модель
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books')
# Без null=True Django требует default для существующих строк
# ВАРИАНТ 1: добавить null=True (если OK иметь книги без владельца)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books', null=True, blank=True)

# ВАРИАНТ 2: в диалоге миграции выбрать "Provide a one-off default"
# и указать id существующего пользователя (например, 1)

# ВАРИАНТ 3: null=True временно, потом убрать после заполнения
# Шаг 1: добавить null=True, мигрировать
# Шаг 2: заполнить owner для всех объектов
# Шаг 3: убрать null=True, мигрировать снова

Ошибка 7: Сравнение owner через id вместо объекта

# НЕПРАВИЛЬНО: сравнение id (работает, но неявно)
return obj.owner_id == request.user.id

# ДОПУСТИМО, но лучше использовать объект
return obj.owner == request.user
# Django сравнивает по pk (id) автоматически при сравнении экземпляров моделей