📖 Теория: Agile Projects ч.2 — ключевые концепции

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

⚡ Ключевые концепции

  • Generic Views: ListCreateAPIView, RetrieveDestroyAPIView — наследование вместо ручного кода
  • Пагинация: PageNumberPagination подключается к APIView через атрибут paginator
  • SlugRelatedField: связь по строковому полю (name, email) вместо pk
  • AbstractBaseUser: полный контроль над моделью пользователя, USERNAME_FIELD = "email"
  • Chunked file save: file.chunks() — запись по частям, избегаем перегрузки памяти

1. Generic Views в DRF

DRF предоставляет готовые классы-«дженерики» для типовых операций. Вместо написания методов get(), post() вручную — наследуемся от готового класса:

Класс HTTP-методы Применение
ListCreateAPIViewGET (список), POSTСписок + создание
RetrieveDestroyAPIViewGET (объект), DELETEПолучение + удаление
RetrieveUpdateDestroyAPIViewGET, PUT, PATCH, DELETEПолный CRUD объекта
ListAPIViewGET (список)Только чтение списка

В проекте «Agile Projects» используется ListCreateAPIView для файлов (список + загрузка) и RetrieveDestroyAPIView для отдельного файла (просмотр + удаление).

2. Пагинация через PageNumberPagination

DRF поддерживает несколько стратегий пагинации. В проекте используется PageNumberPagination — нумерация страниц через query-параметр ?page=N.

При использовании с APIView (не Generic), пагинатор подключается явно:

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'page_size'
    max_page_size = 15

class TasksListAPIView(APIView):
    paginator = StandardResultsSetPagination

    def get(self, request):
        tasks = Task.objects.all()
        paginator = self.paginator()
        page = paginator.paginate_queryset(tasks, request, view=self)
        if page is not None:
            serializer = AllTasksSerializer(page, many=True)
            return paginator.get_paginated_response(serializer.data)
        serializer = AllTasksSerializer(tasks, many=True)
        return Response(serializer.data)

Ответ с пагинацией включает поля count, next, previous, results.

3. SlugRelatedField — связи по строковому полю

SlugRelatedField позволяет отображать и передавать связанные объекты не по pk, а по другому уникальному полю (slug, name, email):

class AllTasksSerializer(serializers.ModelSerializer):
    project = serializers.SlugRelatedField(
        read_only=True,
        slug_field='name'     # отображаем project.name вместо project.id
    )
    assignee = serializers.SlugRelatedField(
        read_only=True,
        slug_field='email'    # отображаем assignee.email
    )

Для записи (при создании) убираем read_only=True и передаём queryset:

project = serializers.SlugRelatedField(
    slug_field='name',
    queryset=Project.objects.all(),  # DRF найдёт объект по имени
)

4. Полевая валидация в сериализаторах

Методы validate_<fieldname> должны быть определены на уровне класса сериализатора, а не внутри class Meta. Это частая ошибка в лекции!

class CreateTaskSerializer(serializers.ModelSerializer):
    class Meta:
        model = Task
        fields = ('name', 'description', 'priority', 'project', 'tags', 'deadline')

    # ПРАВИЛЬНО — на уровне класса, не внутри Meta
    def validate_name(self, value: str) -> str:
        if len(value) < 10:
            raise serializers.ValidationError(
                "The name of the task couldn't be less than 10 characters"
            )
        return value

    def validate_deadline(self, value) -> ...:
        if timezone.is_naive(value):
            value = timezone.make_aware(value, timezone.get_current_timezone())
        if value < timezone.now():
            raise serializers.ValidationError(
                "The deadline of the task couldn't be in the past"
            )
        return value

5. Файловый pipeline: валидация и chunked save

Загрузка файлов требует нескольких уровней защиты:

  1. Валидация имени — только ASCII-символы и допустимые расширения (pdf, csv, doc, xlsx)
  2. Валидация размера — файл ≤ 2 MB
  3. Создание путиdocuments/{project_name}/{file_name}.{ext}
  4. Chunked save — запись через file.chunks(), не загружает всё в память
def save_file(file_path, file_content):
    os.makedirs(os.path.dirname(Path(file_path)), exist_ok=True)
    with open(file_path, 'wb') as f:
        for chunk in file_content.chunks():  # по частям — безопасно
            f.write(chunk)
    return file_path

6. Кастомная модель User

Django позволяет заменить встроенную модель пользователя на свою. Нужно:

  1. Создать приложение users
  2. Унаследоваться от AbstractBaseUser (и опционально PermissionsMixin)
  3. Указать USERNAME_FIELD — поле для входа (у нас email)
  4. Указать REQUIRED_FIELDS — дополнительные поля для createsuperuser
  5. Добавить AUTH_USER_MODEL = 'users.User' в settings.py до первой миграции
  6. Пересоздать базу (удалить старые миграции), провести миграции заново
⚠️ Проверить по документации: в Django 5.x UserManager по умолчанию ожидает поле email как USERNAME_FIELD. Если используется нестандартное имя поля для входа — менеджер нужно переопределить.

7. Вложенные сериализаторы

TaskDetailSerializer включает вложенный ProjectShortInfoSerializer для отображения краткой информации о проекте:

class ProjectShortInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Project
        fields = ('id', 'name')

class TaskDetailSerializer(serializers.ModelSerializer):
    project = ProjectShortInfoSerializer()  # вложенный сериализатор
    tags = TagSerializer(many=True, read_only=True)

    class Meta:
        model = Task
        exclude = ('updated_at', 'deleted_at')

Вложенный сериализатор делает поле только для чтения по умолчанию. Для обновления используется отдельный сериализатор (CreateUpdateTaskSerializer со SlugRelatedField).