✅ Решения: самопроверка DRF-блока

🎯 Разбор ответов К оглавлению урока

⚡ Краткие ответы

  • aggregate vs annotate: aggregate возвращает один словарь; annotate — добавляет поля каждому объекту QuerySet.
  • validated_data: доступен только после успешного is_valid().
  • 201 Created — при успешном POST.
  • ModelViewSet = APIView + все CRUD-методы автоматически.
  • request.query_params.get('author', '') — получить GET-параметр.

Блок А: ORM — агрегация и аннотации

А-1. aggregate vs annotate

aggregate() вычисляет значение по всему QuerySet и возвращает словарь (одна строка результата).

annotate() добавляет вычисленное поле к каждому объекту QuerySet — результат остаётся QuerySet с дополнительным полем.

# aggregate — один словарь
stats = Book.objects.aggregate(total=Count('id'), avg=Avg('price'))
# {'total': 50, 'avg': 19.5}

# annotate — каждому объекту добавляется поле
books = Book.objects.values('author').annotate(cnt=Count('id'))
# [{'author': 'Orwell', 'cnt': 3}, ...]

А-2. Агрегация задач

from django.db.models import Count, Q
from django.utils import timezone

today = timezone.now().date()
stats = Task.objects.aggregate(
    total=Count('id'),
    done=Count('id', filter=Q(status='done')),
    overdue=Count('id', filter=Q(deadline__lt=today) & ~Q(status='done')),
)

А-3. order_by и срезы

# 5 самых дорогих книг
top5 = Book.objects.order_by('-price')[:5]

# + вторичная сортировка по названию
top5 = Book.objects.order_by('-price', 'title')[:5]

А-4. Subquery и OuterRef

OuterRef('field') — ссылка на поле из внешнего запроса внутри подзапроса. Это как коррелированный подзапрос в SQL.

from django.db.models import OuterRef, Subquery, Min

subq = Book.objects.filter(
    author=OuterRef('author')
).values('author').annotate(min_p=Min('price')).values('min_p')

books = Book.objects.annotate(min_author_price=Subquery(subq))

А-5. ExpressionWrapper

ExpressionWrapper нужен, когда Django ORM не может автоматически вывести тип результата выражения (например, при делении DecimalField на DecimalField).

from django.db.models import F, ExpressionWrapper, fields

books = Book.objects.annotate(
    discount_pct=ExpressionWrapper(
        (1 - F('discounted_price') / F('price')) * 100,
        output_field=fields.FloatField()
    )
)

Блок Б: Введение в DRF

Б-1. Зачем DRF

DRF добавляет поверх Django: автоматическую сериализацию/десериализацию, Browsable API, content negotiation, удобные классы views (APIView, Generic, ViewSet), аутентификацию, разрешения, throttling, версионирование.

Б-3. HTTP-статусы

СитуацияСтатус
GET список200 OK
POST — создан объект201 Created
DELETE — удалён204 No Content
Ошибка валидации400 Bad Request
Не найден404 Not Found

Блок В: Сериализаторы

В-2. is_valid и validated_data

is_valid() запускает валидацию: проверяет типы полей, обязательные поля, кастомные валидаторы. Только после успешной валидации данные попадают в validated_data. До вызова is_valid() этот атрибут не существует.

В-4. Кастомная валидация

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

    # Валидация отдельного поля
    def validate_price(self, value):
        if value < 0:
            raise serializers.ValidationError("Цена не может быть отрицательной.")
        return value

    # Валидация нескольких полей
    def validate(self, data):
        discounted = data.get('discounted_price')
        price = data.get('price')
        if discounted and price and discounted >= price:
            raise serializers.ValidationError(
                "Скидочная цена должна быть меньше обычной."
            )
        return data

Блок Г: Представления

Г-3. ModelViewSet + Router

# views.py
from rest_framework import viewsets
from .models import Book
from .serializers import BookSerializer

class BookViewSet(viewsets.ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

# urls.py
from rest_framework.routers import DefaultRouter
from .views import BookViewSet

router = DefaultRouter()
router.register(r'books', BookViewSet, basename='book')
urlpatterns = router.urls

# Генерирует:
# GET  /books/         — list
# POST /books/         — create
# GET  /books/{pk}/    — retrieve
# PUT  /books/{pk}/    — update
# PATCH /books/{pk}/   — partial_update
# DELETE /books/{pk}/  — destroy

Блок Д: query_params

Д-1. Фильтрация

from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Book
from .serializers import BookSerializer

class BookFilterView(APIView):
    def get(self, request):
        qs = Book.objects.all()

        author = request.query_params.get('author')
        if author:
            qs = qs.filter(author__icontains=author)

        is_bestseller = request.query_params.get('is_bestseller')
        if is_bestseller in ('true', '1'):
            qs = qs.filter(is_bestseller=True)

        serializer = BookSerializer(qs, many=True)
        return Response(serializer.data)

Д-2. Пагинация

page = max(1, int(request.query_params.get('page', 1)))
# Ограничить page_size — защита от DoS
page_size = min(100, int(request.query_params.get('page_size', 10)))
start = (page - 1) * page_size
qs = qs[start:start + page_size]

Ограничение min(100, ...) защищает от запроса с ?page_size=100000.