🏠 Домашнее задание 13 — Урок 29

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

📋 Проект: Менеджер задач — сериализаторы и представления ⏱️ Расчётное время: ~90–120 мин

⚡ Суть ДЗ 13

Пять заданий на углубление работы с сериализаторами и CBV в DRF (проект «Менеджер задач»):

  • 1: SubTaskCreateSerializer — поле created_at как read_only
  • 2: CategoryCreateSerializer — методы create/update с проверкой уникальности
  • 3: TaskDetailSerializer — вложенный сериализатор для SubTask
  • 4: TaskCreateSerializer — валидация поля deadline (дата не в прошлом)
  • 5: SubTaskListCreateView + SubTaskDetailUpdateDeleteView на APIView + маршруты

Сдать: код сериализаторов и представлений + скриншоты Postman или VS Code.

📋 Текст задания из LMS

Домашнее задание: Проект "Менеджер задач" — Создание и настройка сериализаторов и добавление представлений
Цель: Освоить настройку сериализаторов для работы с подзадачами и категориями, включая переопределение полей, использование вложенных сериализаторов, методов create и update, а также классы представлений.

Задание 1: Переопределение полей сериализатора

Создайте SubTaskCreateSerializer, в котором поле created_at будет доступно только для чтения (read_only).

Шаги для выполнения:

  1. Определите SubTaskCreateSerializer в файле serializers.py.
  2. Переопределите поле created_at как read_only.

Задание 2: Переопределение методов create и update

Создайте сериализатор для категории CategoryCreateSerializer, переопределив методы create и update для проверки уникальности названия категории. Если категория с таким названием уже существует, возвращайте ошибку валидации.

Шаги для выполнения:

  1. Определите CategoryCreateSerializer в файле serializers.py.
  2. Переопределите метод create для проверки уникальности названия категории.
  3. Переопределите метод update для аналогичной проверки при обновлении.

Задание 3: Использование вложенных сериализаторов

Создайте сериализатор для TaskDetailSerializer, который включает вложенный сериализатор для полного отображения связанных подзадач (SubTask). Сериализатор должен показывать все подзадачи, связанные с данной задачей.

Шаги для выполнения:

  1. Определите TaskDetailSerializer в файле serializers.py.
  2. Вложите SubTaskSerializer внутрь TaskDetailSerializer.

Задание 4: Валидация данных в сериализаторах

Создайте TaskCreateSerializer и добавьте валидацию для поля deadline, чтобы дата не могла быть в прошлом. Если дата в прошлом, возвращайте ошибку валидации.

Шаги для выполнения:

  1. Определите TaskCreateSerializer в файле serializers.py.
  2. Переопределите метод validate_deadline для проверки даты.

Задание 5: Создание классов представлений

Создайте классы представлений для работы с подзадачами (SubTasks), включая создание, получение, обновление и удаление подзадач. Используйте классы представлений (APIView) для реализации этого функционала.

Шаги для выполнения:

  1. Создайте класс представления для создания и получения списка подзадач (SubTaskListCreateView).
  2. Создайте класс представления для получения, обновления и удаления подзадач (SubTaskDetailUpdateDeleteView).
  3. Добавьте маршруты в файле urls.py, чтобы использовать эти классы.
Оформите ваш ответ следующим образом:
  • Код сериализаторов и представлений: вставьте весь код из serializers.py и views.py.
  • Скриншоты ручного тестирования: приложите скриншоты консоли или Postman, подтверждающие успешное выполнение запросов.

🛠️ Подготовка окружения

Шаг 1: Создание виртуального окружения и проекта

Если продолжаете проект из ДЗ 12 — пропустите этот шаг. Если начинаете с нуля:

# Создаём папку проекта
mkdir task_manager
cd task_manager

# Создаём venv
python -m venv venv

# Активируем (Windows PowerShell)
.\venv\Scripts\Activate.ps1

# Если ошибка ExecutionPolicy:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Шаг 2: Установка зависимостей

pip install django djangorestframework

# Фиксируем
pip freeze > requirements.txt

Шаг 3: Создание проекта и приложения

django-admin startproject config .
python manage.py startapp tasks

Шаг 4: Настройка settings.py

# config/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',   # DRF
    'tasks',            # наше приложение
]

Шаг 5: Git-инициализация

git init
git add .
git commit -m "initial: Django + DRF task manager"

📐 Модели проекта

ДЗ 13 предполагает, что модели уже созданы (из ДЗ 12). Для нового проекта — создайте их:

# tasks/models.py
from django.db import models
from django.utils import timezone


class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)

    def __str__(self):
        return self.name


class Task(models.Model):
    STATUS_CHOICES = [
        ('todo', 'To Do'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ]

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True, default='')
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='todo'
    )
    deadline = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='tasks'
    )

    def __str__(self):
        return self.title


class SubTask(models.Model):
    STATUS_CHOICES = Task.STATUS_CHOICES

    title = models.CharField(max_length=200)
    description = models.TextField(blank=True, default='')
    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default='todo'
    )
    task = models.ForeignKey(
        Task,
        on_delete=models.CASCADE,
        related_name='subtasks'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    deadline = models.DateField(null=True, blank=True)

    def __str__(self):
        return f"{self.title} (подзадача к: {self.task.title})"
# После создания моделей:
python manage.py makemigrations tasks
python manage.py migrate

✅ Пошаговое решение

Задание 1: SubTaskCreateSerializer — поле read_only

Связь с теорией: read_only_fields и явное переопределение поля в сериализаторе (см. theory.html — Поля для отношений).

# tasks/serializers.py
from rest_framework import serializers
from .models import SubTask, Task, Category
from django.utils import timezone


class SubTaskCreateSerializer(serializers.ModelSerializer):
    """
    Задание 1: поле created_at — только для чтения.
    DRF не позволит клиенту установить created_at при POST/PUT.
    """
    created_at = serializers.DateTimeField(read_only=True)

    class Meta:
        model = SubTask
        fields = ['id', 'title', 'description', 'status', 'task', 'deadline', 'created_at']

Альтернативный способ через read_only_fields:

class SubTaskCreateSerializer(serializers.ModelSerializer):
    class Meta:
        model = SubTask
        fields = ['id', 'title', 'description', 'status', 'task', 'deadline', 'created_at']
        read_only_fields = ['created_at']  # то же самое, короче

Задание 2: CategoryCreateSerializer — методы create и update

Связь с теорией: переопределение методов сохранения в сериализаторе (см. theory.html).

class CategoryCreateSerializer(serializers.ModelSerializer):
    """
    Задание 2: уникальность названия категории при create и update.
    """
    class Meta:
        model = Category
        fields = ['id', 'name']

    def create(self, validated_data):
        name = validated_data.get('name')
        # Проверяем уникальность при создании
        if Category.objects.filter(name=name).exists():
            raise serializers.ValidationError(
                {'name': f'Категория с названием "{name}" уже существует.'}
            )
        return super().create(validated_data)

    def update(self, instance, validated_data):
        name = validated_data.get('name', instance.name)
        # Проверяем уникальность при обновлении (исключаем текущий объект)
        if Category.objects.filter(name=name).exclude(pk=instance.pk).exists():
            raise serializers.ValidationError(
                {'name': f'Категория с названием "{name}" уже существует.'}
            )
        return super().update(instance, validated_data)

Задание 3: TaskDetailSerializer — вложенный сериализатор

Связь с примерами: вложенные сериализаторы для отображения связанных объектов (см. examples.html).

class SubTaskSerializer(serializers.ModelSerializer):
    """Базовый сериализатор для SubTask — используется как вложенный."""
    class Meta:
        model = SubTask
        fields = ['id', 'title', 'description', 'status', 'deadline', 'created_at']


class TaskDetailSerializer(serializers.ModelSerializer):
    """
    Задание 3: TaskDetail — включает все подзадачи полностью.
    subtasks — вложенный сериализатор, many=True, read_only=True.
    """
    subtasks = SubTaskSerializer(many=True, read_only=True)

    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'status', 'deadline', 'created_at', 'category', 'subtasks']
Как работает вложенный сериализатор:
  • subtasks — имя совпадает с related_name='subtasks' в модели SubTask
  • many=True — SubTask их может быть несколько
  • read_only=True — подзадачи создаются отдельно, не через TaskDetail

Задание 4: TaskCreateSerializer — валидация deadline

Связь с теорией: методы validate_<field>() для валидации отдельных полей (см. theory.html).

class TaskCreateSerializer(serializers.ModelSerializer):
    """
    Задание 4: deadline не может быть в прошлом.
    """
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'status', 'deadline', 'category']

    def validate_deadline(self, value):
        """
        Валидатор для поля deadline.
        DRF автоматически вызывает validate_ перед сохранением.
        """
        if value and value < timezone.now().date():
            raise serializers.ValidationError(
                'Дата дедлайна не может быть в прошлом.'
            )
        return value

Задание 5: SubTaskListCreateView + SubTaskDetailUpdateDeleteView

Связь с теорией: APIView — методы get/post/put/delete (см. theory.html#apiview).

# tasks/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

from .models import SubTask, Task
from .serializers import SubTaskCreateSerializer, SubTaskSerializer


class SubTaskListCreateView(APIView):
    """
    Задание 5a: список и создание подзадач.
    GET  /api/subtasks/        — список всех подзадач
    POST /api/subtasks/        — создание новой подзадачи
    """

    def get(self, request):
        subtasks = SubTask.objects.select_related('task').all()
        serializer = SubTaskSerializer(subtasks, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = SubTaskCreateSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class SubTaskDetailUpdateDeleteView(APIView):
    """
    Задание 5b: получение, обновление и удаление одной подзадачи.
    GET    /api/subtasks/{pk}/   — одна подзадача
    PUT    /api/subtasks/{pk}/   — полное обновление
    PATCH  /api/subtasks/{pk}/   — частичное обновление
    DELETE /api/subtasks/{pk}/   — удаление
    """

    def get_object(self, pk):
        try:
            return SubTask.objects.get(pk=pk)
        except SubTask.DoesNotExist:
            return None

    def get(self, request, pk):
        subtask = self.get_object(pk)
        if subtask is None:
            return Response(
                {'error': f'SubTask с id={pk} не найден.'},
                status=status.HTTP_404_NOT_FOUND
            )
        serializer = SubTaskSerializer(subtask)
        return Response(serializer.data)

    def put(self, request, pk):
        subtask = self.get_object(pk)
        if subtask is None:
            return Response(
                {'error': f'SubTask с id={pk} не найден.'},
                status=status.HTTP_404_NOT_FOUND
            )
        serializer = SubTaskCreateSerializer(subtask, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def patch(self, request, pk):
        subtask = self.get_object(pk)
        if subtask is None:
            return Response(
                {'error': f'SubTask с id={pk} не найден.'},
                status=status.HTTP_404_NOT_FOUND
            )
        serializer = SubTaskCreateSerializer(subtask, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        subtask = self.get_object(pk)
        if subtask is None:
            return Response(
                {'error': f'SubTask с id={pk} не найден.'},
                status=status.HTTP_404_NOT_FOUND
            )
        subtask.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

Маршруты (urls.py)

# tasks/urls.py
from django.urls import path
from .views import SubTaskListCreateView, SubTaskDetailUpdateDeleteView

urlpatterns = [
    path('subtasks/', SubTaskListCreateView.as_view(), name='subtask-list-create'),
    path('subtasks/<int:pk>/', SubTaskDetailUpdateDeleteView.as_view(), name='subtask-detail'),
]
# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('tasks.urls')),
]

Запуск сервера

python manage.py runserver

🔍 Проверка в VS Code и Postman

Запуск через VS Code (F5)

Создайте файл .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Django runserver",
      "type": "debugpy",
      "request": "launch",
      "program": "${workspaceFolder}/manage.py",
      "args": ["runserver"],
      "django": true,
      "justMyCode": true
    }
  ]
}

Нажмите F5 — сервер запустится в режиме отладки. Для точек останова: кликните слева от строки в views.py или serializers.py, выполните запрос — VS Code остановится на точке и покажет значения переменных.

Тестирование через Postman

Тест 1: Создание подзадачи (Задание 5)

  • Метод: POST
  • URL: http://127.0.0.1:8000/api/subtasks/
  • Headers: Content-Type: application/json
  • Body (raw JSON):
    {
      "title": "Написать тесты для API",
      "description": "Unit тесты на pytest",
      "status": "todo",
      "task": 1,
      "deadline": "2026-07-15"
    }
  • Ожидаемый ответ: 201 Createdcreated_at выставляется сервером, в запросе игнорируется (Задание 1)

Тест 2: Список подзадач (GET)

  • Метод: GET
  • URL: http://127.0.0.1:8000/api/subtasks/
  • Ожидаемый ответ: 200 OK — массив подзадач

Тест 3: Валидация deadline в прошлом (Задание 4)

  • Метод: POST
  • URL: http://127.0.0.1:8000/api/tasks/ (если добавили TaskCreateSerializer в views)
  • Body:
    {
      "title": "Просроченная задача",
      "status": "todo",
      "deadline": "2020-01-01"
    }
  • Ожидаемый ответ: 400 Bad Request:
    {
      "deadline": ["Дата дедлайна не может быть в прошлом."]
    }

Тест 4: Уникальность категории (Задание 2)

  • Первый POST — создаём категорию "Работа" → 201 Created
  • Второй POST с тем же именем → 400 Bad Request:
    {
      "name": ["Категория с названием \"Работа\" уже существует."]
    }

Тест 5: TaskDetailSerializer с вложенными SubTask (Задание 3)

  • Метод: GET
  • URL: http://127.0.0.1:8000/api/tasks/1/ (если добавили соответствующий view)
  • Ожидаемый ответ: 200 OK — объект Task с массивом subtasks внутри

Тест 6: Удаление подзадачи (DELETE)

  • Метод: DELETE
  • URL: http://127.0.0.1:8000/api/subtasks/1/
  • Ожидаемый ответ: 204 No Content — пустое тело

Тестирование через терминал (curl)

# Создать подзадачу
curl -X POST http://127.0.0.1:8000/api/subtasks/ ^
  -H "Content-Type: application/json" ^
  -d "{\"title\": \"Test subtask\", \"status\": \"todo\", \"task\": 1}"

# Список подзадач
curl http://127.0.0.1:8000/api/subtasks/

# Получить одну подзадачу
curl http://127.0.0.1:8000/api/subtasks/1/

# Удалить
curl -X DELETE http://127.0.0.1:8000/api/subtasks/1/