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

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

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

  • None в filter(): добавляем параметр в filters без проверки — filter(author=None) возвращает пустой queryset
  • Строка в year-фильтре: filter(published_date__year="2023") может работать, но filter(published_date__year=None) сломает запрос
  • Сортировка без валидации: order_by(sort_by) без белого списка — клиент может передать несуществующее поле

Ошибка 1: None в словаре filters

Проблема

# НЕПРАВИЛЬНО — параметр добавляется всегда
class BookListView(APIView):
    def get(self, request):
        author = request.query_params.get('author')
        filters = {'author': author}  # author может быть None!
        books = Book.objects.filter(**filters)
        ...

Если параметр не передан, author = None, и filter(author=None) вернёт только книги с author=None или пустой queryset.

Решение

# ПРАВИЛЬНО — добавляем только при наличии значения
author = request.query_params.get('author')
filters = {}
if author:
    filters['author'] = author
books = Book.objects.filter(**filters)  # без фильтра = все книги

Ошибка 2: Числовые параметры без преобразования типа

Проблема

# НЕПРАВИЛЬНО — год как строка
year = request.query_params.get('year')
filters = {}
if year:
    filters['published_date__year'] = year  # строка "2023"

# ХУЖЕ — год без проверки isdigit()
if year:
    filters['published_date__year'] = int(year)  # ValueError если year="abc"

Решение

# ПРАВИЛЬНО
year = request.query_params.get('year')
filters = {}
if year and year.isdigit():
    filters['published_date__year'] = int(year)
elif year:
    # Неверный формат — можно вернуть ошибку
    return Response(
        {'error': 'year must be a number'},
        status=400
    )

Ошибка 3: Сортировка без валидации поля

Проблема

# НЕПРАВИЛЬНО — клиент может передать произвольное поле
sort_by = request.query_params.get('sort_by', 'title')
books = Book.objects.order_by(sort_by)
# ?sort_by=password_hash — раскрывает схему
# ?sort_by=nonexistent — FieldError

Решение

ALLOWED_SORT_FIELDS = {'title', 'price', 'published_date', 'author'}
sort_by = request.query_params.get('sort_by', 'title')
if sort_by.lstrip('-') not in ALLOWED_SORT_FIELDS:
    sort_by = 'title'  # сброс к дефолту
books = Book.objects.order_by(sort_by)

Ошибка 4: Пагинация без проверки results на None

Проблема

# НЕПРАВИЛЬНО — paginate_queryset может вернуть None
class BookListView(APIView, PageNumberPagination):
    page_size = 5
    def get(self, request):
        books = Book.objects.all()
        results = self.paginate_queryset(books, request, view=self)
        # Если results = None (нет пагинации) — serializer получит None
        serializer = BookDetailSerializer(results, many=True)
        return self.get_paginated_response(serializer.data)

paginate_queryset возвращает None, если пагинация не применяется (например, параметр page не передан и page_size не ограничивает). В этом случае нужна ветка для обычного ответа.

Решение

def get(self, request):
    books = Book.objects.all()
    results = self.paginate_queryset(books, request, view=self)
    if results is not None:
        serializer = BookDetailSerializer(results, many=True)
        return self.get_paginated_response(serializer.data)
    # Пагинация не применена — обычный ответ
    serializer = BookDetailSerializer(books, many=True)
    return Response(serializer.data)

Ошибка 5: Использование request.data вместо request.query_params

Проблема

# НЕПРАВИЛЬНО — request.data для GET-параметров
class BookListView(APIView):
    def get(self, request):
        author = request.data.get('author')  # всегда None для GET!

request.data содержит тело запроса (POST, PUT, PATCH). Для GET-запросов тело обычно пустое. Параметры URL нужно читать через request.query_params.

Решение

# ПРАВИЛЬНО
author = request.query_params.get('author')  # из URL ?author=...

Ошибка 6: ExtractWeekDay — неочевидная нумерация

Проблема

# Ожидаем: понедельник = 1
tasks = Task.objects.filter(due_date__week_day=1)
# Получаем: воскресенье!

# ExtractWeekDay использует нумерацию:
# 1 = Воскресенье, 2 = Понедельник, ..., 7 = Суббота
# (ISO: 1 = Понедельник, 7 = Воскресенье)

Решение

# Используйте словарь-маппинг
WEEKDAY_MAP = {
    'понедельник': 2,   # Django: 2
    'вторник': 3,
    'среда': 4,
    'четверг': 5,
    'пятница': 6,
    'суббота': 7,
    'воскресенье': 1,   # Django: 1
}

weekday = request.query_params.get('weekday', '').lower()
weekday_num = WEEKDAY_MAP.get(weekday)
if weekday_num:
    tasks = Task.objects.filter(due_date__week_day=weekday_num)
⚠️ Проверить по документации: нумерация WeekDay в Django может зависеть от настройки USE_TZ и базы данных.

Ошибка 7: Забыть .as_view() в urls.py

Проблема

# НЕПРАВИЛЬНО — класс без .as_view()
urlpatterns = [
    path('books/', BookListView),  # TypeError!
]

Решение

# ПРАВИЛЬНО
urlpatterns = [
    path('books/', BookListView.as_view(), name='book-list'),
]